软件工程第一次作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/15712 |
| 这个作业的目标 | huggingface的API调用,搭建Github的个人主页,熟悉博客园 |
| 学号 | 102401507 |
1.准备工作
-
已注册Github账号并完善个人信息
-
已注册博客园账号并完善个人信息
-
已关注老师和助教的博客并加入班级
2.huggingface-API的调用
2.1在hugging face官网注册个人账号并获取API

2.2操作步骤
- 安装依赖并把 Token 写进
.env文件:
python -m pip install -r requirements.txt
Copy-Item .env.example .env
notepad .env # 写入 HF_TOKEN=hf_你自己的Token
- 先用命令行脚本单独验证接口能不能调通:
python generate_image.py "a girl reading book" 20260910 768 20
- 启动后端服务,浏览器访问页面进行交互生成:
python app.py
# 出现 "Flux 写实图像生成器已启动:http://127.0.0.1:5000" 后,在浏览器打开该地址
2.3 后端代码
后端的职责是三件事:接收前端的参数、调用模型接口、把图片保存下来并返回给页面。核心调用代码如下:
client = InferenceClient(provider=PROVIDER, api_key=token, timeout=180)
call_params = {
"model": MODEL_ID, # "XLabs-AI/flux-RealismLora"
"negative_prompt": negative_prompt,
"width": width,
"height": height,
"num_inference_steps": steps,
"guidance_scale": guidance,
}
if seed:
call_params["seed"] = int(seed) # 固定种子,结果可复现
image = client.text_to_image(prompt, **call_params)
image.save(out_file, format="PNG") # 保存到 outputs 目录
buf = io.BytesIO()
image.save(buf, format="PNG")
img_base64 = base64.b64encode(buf.getvalue()).decode("utf-8")
点击展开:app.py 完整代码(Flask 后端)
# -*- coding: utf-8 -*-
import base64
import datetime
import io
import json
import os
import time
from pathlib import Path
from dotenv import load_dotenv
from flask import Flask, jsonify, render_template, request
from huggingface_hub import InferenceClient
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
MODEL_ID = os.getenv("HF_MODEL_ID", "XLabs-AI/flux-RealismLora")
PROVIDER = os.getenv("HF_PROVIDER", "fal-ai")
OUTPUT_DIR = BASE_DIR / "outputs"
LOG_FILE = BASE_DIR / "calls_log.jsonl"
OUTPUT_DIR.mkdir(exist_ok=True)
app = Flask(__name__)
_client = None
def get_client():
# 第一次用到的时候才创建,顺便检查 Token
global _client
if _client is None:
token = os.getenv("HF_TOKEN", "").strip()
if not token:
raise RuntimeError("没有找到 HF_TOKEN,请先配置 .env")
_client = InferenceClient(provider=PROVIDER, api_key=token, timeout=180)
return _client
def write_log(record):
with LOG_FILE.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/latest")
def latest():
# 读日志里最近一次成功的结果给页面显示,不调用 API 也不花钱
if not LOG_FILE.exists():
return jsonify({"success": False, "error": "还没有调用记录"})
for line in reversed(LOG_FILE.read_text(encoding="utf-8").splitlines()):
line = line.strip()
if not line:
continue
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
if rec.get("status") != "success":
continue
out_path = Path(rec.get("output", ""))
if not out_path.is_file():
continue
return jsonify({
"success": True,
"image_base64": base64.b64encode(out_path.read_bytes()).decode("utf-8"),
"file": str(out_path),
"time": rec.get("time"),
"elapsed_seconds": rec.get("elapsed_seconds"),
"width": rec.get("width"),
"height": rec.get("height"),
})
return jsonify({"success": False, "error": "没有找到成功记录"})
@app.route("/api/generate", methods=["POST"])
def generate():
data = request.get_json(silent=True) or {}
prompt = (data.get("prompt") or "").strip()
if not prompt:
return jsonify({"error": "提示词不能为空"}), 400
negative_prompt = (data.get("negative_prompt") or "").strip()
width = int(data.get("width", 768))
height = int(data.get("height", 768))
steps = int(data.get("steps", 20))
guidance = float(data.get("guidance", 3.5))
seed = (data.get("seed") or "").strip()
params = {
"model": MODEL_ID,
"negative_prompt": negative_prompt,
"width": width,
"height": height,
"num_inference_steps": steps,
"guidance_scale": guidance,
}
# 留空就是每次随机,填了数字结果就能复现
if seed:
params["seed"] = int(seed)
record = {
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"model": MODEL_ID,
"provider": PROVIDER,
"prompt": prompt,
"negative_prompt": negative_prompt,
"width": width,
"height": height,
"steps": steps,
"guidance": guidance,
"seed": params.get("seed"),
}
start = time.time()
try:
image = get_client().text_to_image(prompt, **params)
elapsed = round(time.time() - start, 2)
except Exception as exc:
elapsed = round(time.time() - start, 2)
record.update({"status": "failed", "elapsed_seconds": elapsed, "error": str(exc)})
write_log(record)
print("[失败] 耗时 %ss:%s" % (elapsed, exc))
return jsonify({"error": "API 调用失败:" + str(exc)}), 502
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
out_file = OUTPUT_DIR / ("flux_realism_%s.png" % stamp)
image.save(out_file, format="PNG")
buf = io.BytesIO()
image.save(buf, format="PNG")
img_base64 = base64.b64encode(buf.getvalue()).decode("utf-8")
record.update({
"status": "success",
"elapsed_seconds": elapsed,
"output": str(out_file),
"file_size_bytes": out_file.stat().st_size,
})
write_log(record)
print("[成功] 耗时 %ss,已保存 %s" % (elapsed, out_file))
return jsonify({
"success": True,
"image_base64": img_base64,
"file": str(out_file),
"elapsed_seconds": elapsed,
})
if __name__ == "__main__":
print("Flux 写实图像生成器已启动:http://127.0.0.1:5000")
app.run(host="127.0.0.1", port=5000)
点击展开:generate_image.py 完整代码(命令行验证脚本)
# -*- coding: utf-8 -*-
# 不启动网页,单独跑一次看看接口能不能调通
# 用法:python generate_image.py "提示词" 种子 宽 高 步数
import datetime
import json
import os
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
from huggingface_hub import InferenceClient
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
MODEL_ID = os.getenv("HF_MODEL_ID", "XLabs-AI/flux-RealismLora")
PROVIDER = os.getenv("HF_PROVIDER", "fal-ai")
PROMPT = "a girl reading book"
NEGATIVE = ("cartoon, anime, illustration, painting, drawing, CGI, 3D render, "
"oversaturated colors, beauty filter, airbrushed, smooth plastic skin, "
"deformed hands, extra fingers, watermark, text, logo")
def main():
token = os.getenv("HF_TOKEN", "").strip()
if not token:
print("没有找到 HF_TOKEN,请先在 .env 里填好")
sys.exit(1)
prompt = sys.argv[1] if len(sys.argv) > 1 else PROMPT
seed = sys.argv[2] if len(sys.argv) > 2 else None
width = int(sys.argv[3]) if len(sys.argv) > 3 else 768
height = int(sys.argv[4]) if len(sys.argv) > 4 else 768
steps = int(sys.argv[5]) if len(sys.argv) > 5 else 20
client = InferenceClient(provider=PROVIDER, api_key=token, timeout=180)
params = {
"model": MODEL_ID,
"negative_prompt": NEGATIVE,
"width": width,
"height": height,
"num_inference_steps": steps,
"guidance_scale": 3.5,
}
if seed:
params["seed"] = int(seed)
print("正在调用 %s ..." % MODEL_ID)
start = time.time()
image = client.text_to_image(prompt, **params)
elapsed = round(time.time() - start, 2)
out_dir = BASE_DIR / "outputs"
out_dir.mkdir(exist_ok=True)
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
out_file = out_dir / ("flux_realism_%s.png" % stamp)
image.save(out_file, format="PNG")
record = {
"time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"model": MODEL_ID,
"provider": PROVIDER,
"status": "success",
"elapsed_seconds": elapsed,
"width": width,
"height": height,
"steps": steps,
"seed": params.get("seed"),
"prompt": prompt,
"output": str(out_file),
"file_size_bytes": out_file.stat().st_size,
}
with (BASE_DIR / "calls_log.jsonl").open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
print("[成功] API 调用成功,耗时 %s 秒" % elapsed)
print("[成功] 图片已保存:" + str(out_file))
if __name__ == "__main__":
main()
2.4前端代码
前端页面用 HTML、CSS、JavaScript 写在同一个 index.html 里:左侧是提示词输入框和参数调节,右侧显示生成结果。点击按钮后,用 fetch 把参数打包成 JSON 发给后端:
const resp = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: promptEl.value, // 提示词
negative_prompt: negEl.value, // 负向提示词
seed: seedEl.value, // 随机种子,留空表示每次随机
width: widthEl.value,
height: heightEl.value,
steps: stepsEl.value,
guidance: guidanceEl.value
})
});
const data = await resp.json();
resultImg.src = "data:image/png;base64," + data.image_base64;
点击展开:templates/index.html 完整代码(前端页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hugging Face × FLUX 写实图像生成</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
background: #0f1420;
color: #e8ecf5;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 40px 18px;
}
.page { width: 100%; max-width: 980px; }
h1 { font-size: 22px; font-weight: 600; text-align: center; }
.sub { text-align: center; color: #7d8ba3; font-size: 13px; margin: 8px 0 26px; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
.card {
background: #171f30;
border: 1px solid #253250;
border-radius: 12px;
padding: 20px;
}
label { display: block; font-size: 13px; color: #93a1b8; margin: 14px 0 6px; }
textarea, input, select {
width: 100%;
background: #0f1522;
border: 1px solid #2b3a5a;
color: #e8ecf5;
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
font-family: inherit;
}
textarea { resize: vertical; min-height: 92px; line-height: 1.6; }
input:focus, textarea:focus, select:focus { outline: 2px solid #3b6fe0; border-color: transparent; }
.row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; }
.slider-row { display: flex; align-items: center; gap: 12px; }
.slider-row input[type=range] { flex: 1; padding: 0; }
.slider-row output { min-width: 34px; text-align: right; color: #8ab4ff; font-weight: 600; }
.check {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
font-size: 13px;
color: #93a1b8;
}
.check input[type=checkbox] {
width: 15px; height: 15px; flex: none; margin: 0; padding: 0;
accent-color: #3567d8;
}
button {
background: #2b3a5a;
color: #e8ecf5;
border: 1px solid #3a4d75;
border-radius: 8px;
padding: 9px 14px;
font-size: 14px;
cursor: pointer;
transition: background .15s;
}
button:hover { background: #3a4d75; }
button.primary {
width: 100%;
margin-top: 18px;
background: linear-gradient(135deg, #3567d8, #2450b8);
border: none;
font-size: 16px;
font-weight: 600;
padding: 13px;
}
button.primary:hover:not(:disabled) { background: linear-gradient(135deg, #4375ea, #2e5fce); }
button.primary:disabled { opacity: .6; cursor: not-allowed; }
#status { margin-top: 14px; font-size: 14px; min-height: 22px; color: #93a1b8; }
#status.loading { color: #f5b15c; }
#status.success { color: #67d39a; }
#status.error { color: #f27d7d; }
.result-box {
display: flex;
flex-direction: column;
min-height: 420px;
justify-content: center;
align-items: center;
background: #0b111d;
border: 1px dashed #2b3a5a;
border-radius: 10px;
overflow: hidden;
}
.result-box img { max-width: 100%; max-height: 560px; display: none; object-fit: contain; }
.placeholder { color: #5b6b86; text-align: center; font-size: 14px; padding: 20px; }
#meta {
font-size: 12px;
color: #8b99b0;
margin-top: 12px;
line-height: 1.8;
word-break: break-all;
white-space: pre-wrap;
}
.download-wrap { display: flex; margin-top: 12px; }
.download-wrap button { flex: 1; }
@media (max-width: 820px) {
.layout { grid-template-columns: 1fr; }
.row-3 { grid-template-columns: 1fr 1fr; }
}
</style>
</head>
<body>
<div class="page">
<h1>Hugging Face × FLUX 写实图像生成</h1>
<p class="sub">XLabs-AI/flux-RealismLora · Flask + HF Inference Providers</p>
<div class="layout">
<section class="card">
<label for="prompt">提示词 Prompt</label>
<textarea id="prompt" spellcheck="false" placeholder="例如:A realistic photo of an old man feeding pigeons in a park at sunrise"></textarea>
<label for="neg">负向提示词 Negative Prompt</label>
<textarea id="neg" spellcheck="false"></textarea>
<div class="row-3">
<div>
<label for="width">宽度</label>
<select id="width">
<option value="512">512</option>
<option value="768" selected>768</option>
<option value="1024">1024</option>
<option value="1152">1152</option>
</select>
</div>
<div>
<label for="height">高度</label>
<select id="height">
<option value="512">512</option>
<option value="768" selected>768</option>
<option value="1024">1024</option>
<option value="1152">1152</option>
</select>
</div>
<div>
<label for="seed">随机种子</label>
<input id="seed" type="text" placeholder="留空 = 随机">
</div>
</div>
<label for="steps">推理步数</label>
<div class="slider-row">
<input id="steps" type="range" min="12" max="50" value="20">
<output id="stepsOut">20</output>
</div>
<label for="guidance">引导系数</label>
<div class="slider-row">
<input id="guidance" type="range" min="1" max="10" step="0.5" value="3.5">
<output id="guidanceOut">3.5</output>
</div>
<label class="check" for="cheapMode">
<input id="cheapMode" type="checkbox" checked>
省额度模式(768×768 / 20 步)
</label>
<button class="primary" id="goBtn">开始生成图像</button>
<div id="status">就绪</div>
</section>
<section class="card">
<div class="result-box" id="resultBox">
<div class="placeholder" id="placeholder">等待生成……</div>
<img id="resultImg" alt="生成的写实图像">
</div>
<div id="meta"></div>
<div class="download-wrap" id="downloadWrap" style="display:none;">
<button id="downloadBtn">下载 PNG</button>
</div>
</section>
</div>
</div>
<script>
const DEFAULT_NEGATIVE = "cartoon, anime, illustration, painting, drawing, CGI, 3D render, oversaturated colors, beauty filter, airbrushed, smooth plastic skin, deformed hands, extra fingers, watermark, text, logo";
const promptEl = document.getElementById("prompt");
const negEl = document.getElementById("neg");
const seedEl = document.getElementById("seed");
const widthEl = document.getElementById("width");
const heightEl = document.getElementById("height");
const stepsEl = document.getElementById("steps");
const guidanceEl = document.getElementById("guidance");
const stepsOut = document.getElementById("stepsOut");
const guidanceOut = document.getElementById("guidanceOut");
const cheapEl = document.getElementById("cheapMode");
const goBtn = document.getElementById("goBtn");
const statusEl = document.getElementById("status");
const placeholder = document.getElementById("placeholder");
const resultImg = document.getElementById("resultImg");
const metaEl = document.getElementById("meta");
const downloadWrap = document.getElementById("downloadWrap");
const downloadBtn = document.getElementById("downloadBtn");
negEl.value = DEFAULT_NEGATIVE;
stepsEl.addEventListener("input", () => stepsOut.textContent = stepsEl.value);
guidanceEl.addEventListener("input", () => guidanceOut.textContent = guidanceEl.value);
// 省额度模式
const CHEAP_PRESET = { width: "768", height: "768", steps: "20" };
const FULL_PRESET = { width: "1024", height: "1024", steps: "28" };
function applyPreset(preset) {
widthEl.value = preset.width;
heightEl.value = preset.height;
stepsEl.value = preset.steps;
stepsOut.textContent = preset.steps;
}
cheapEl.addEventListener("change", () => {
applyPreset(cheapEl.checked ? CHEAP_PRESET : FULL_PRESET);
});
function showImage(base64) {
resultImg.src = "data:image/png;base64," + base64;
resultImg.style.display = "block";
placeholder.style.display = "none";
downloadWrap.style.display = "flex";
}
(async () => {
try {
const resp = await fetch("/api/latest");
const data = await resp.json();
if (!data.success) return;
showImage(data.image_base64);
statusEl.textContent = "已载入上次生成结果(" + (data.time || "") + ")";
metaEl.textContent =
"模型:XLabs-AI/flux-RealismLora\n" +
"尺寸:" + (data.width || "?") + " x " + (data.height || "?") + "\n" +
"耗时:" + (data.elapsed_seconds || "?") + " 秒\n" +
"文件:" + data.file;
} catch (err) {
}
})();
goBtn.addEventListener("click", async () => {
const prompt = promptEl.value.trim();
if (!prompt) {
statusEl.className = "error";
statusEl.textContent = "请先输入提示词";
return;
}
goBtn.disabled = true;
statusEl.className = "loading";
statusEl.textContent = "正在生成,请勿重复点击……";
metaEl.textContent = "";
try {
const resp = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: prompt,
negative_prompt: negEl.value.trim(),
seed: seedEl.value.trim(),
width: widthEl.value,
height: heightEl.value,
steps: stepsEl.value,
guidance: guidanceEl.value
})
});
const data = await resp.json();
if (!resp.ok) throw new Error(data.error || ("HTTP " + resp.status));
showImage(data.image_base64);
statusEl.className = "success";
statusEl.textContent = "生成成功,耗时 " + data.elapsed_seconds + " 秒";
metaEl.textContent =
"尺寸:" + widthEl.value + " x " + heightEl.value + "\n" +
"步数:" + stepsEl.value + "\n" +
"文件:" + data.file;
} catch (err) {
statusEl.className = "error";
statusEl.textContent = "生成失败:" + err.message;
} finally {
goBtn.disabled = false;
}
});
downloadBtn.addEventListener("click", () => {
const a = document.createElement("a");
a.href = resultImg.src;
a.download = "flux_realism_result.png";
document.body.appendChild(a);
a.click();
a.remove();
});
</script>
</body>
</html>
2.5API 调用成功记录

2.6 提示词设计思路和修改过程
XLabs-AI/flux-RealismLora 是在 FLUX.1-dev 基础上增强照片真实感的模型。想让结果贴近真实世界,堆"高清""8K"这类形容词没有用,要把提示词写成一份给摄影师的需求单,逐项交代六类信息:拍摄类型、主体与动作、环境、光线、材质细节、摄影语言。再用负向提示词排掉最容易破坏真实感的元素:卡通、插画、CGI、美颜磨皮、塑料皮肤、变形手指、水印文字。
第一版:只写主体
a girl reading book


只有几个单词,没有交代在哪、什么光、什么质感,生成结果偏概念化,光线均匀,缺少真实照片的信息量。
第二版:补充细节
A candid documentary photograph of a seven-year-old girl reading a picture book on her
bed in her own bedroom. Warm afternoon sunlight comes through half-open curtains and falls
across the open pages and her bare feet. Toys and crayons lie scattered on the blanket, a
worn stuffed rabbit sits by her pillow. Natural skin texture, messy pigtails, a faded
cotton pyjama with a small stain on the sleeve. She lies on her stomach with her chin
propped on one hand, absorbed in the book. Muted natural colours, 35mm film look, shallow
depth of field, slight grain, realistic proportions, photorealistic.
| 维度 | 第一版 | 第二版补充的内容 | 预期改进 |
|---|---|---|---|
| 拍摄类型 | 无 | candid documentary photograph |
明确纪实抓拍口吻,不会画成插画 |
| 时间地点 | 无 | 自己在家里,趴在卧室的床上,午后 | 画面有具体场景,不再空洞泛化 |
| 光线 | 无 | 午后阳光从半开的窗帘透进来,落在翻开的书页和光着的脚上 | 出现明确的光源方向和明暗层次 |
| 材质细节 | 无 | 真实皮肤质感、凌乱的马尾辫、洗旧棉睡衣袖子上的污渍、被子上散落的玩具和蜡笔、枕边的旧玩偶兔 | 消除 AI 常见的塑料皮肤感,画面有生活痕迹 |
| 姿态 | 无 | 趴在床上,一只手掌托着下巴,沉浸在书里 | 动作自然,不像摆拍模特 |
| 镜头语言 | 无 | 35mm 胶片、浅景深、轻微颗粒、哑光自然色 | 更接近相机拍摄的质感 |
- 第一版只说了"谁在做什么",模型只能靠默认想象补画面;第二版补上了环境、光源方向、生活痕迹和身体姿态,让画面有了更多细节。但是人物看起来还是不太真实,不过免费额度不够了TT

2.7 体验与心得
前后端分离不是形式主义。 Token 写在前端,任何人打开浏览器开发者工具就能拿走;只有让后端做代理、把 Token 放在服务端环境变量里,调用才是安全的。
提示词工程本质上是写需求。 想让模型生成照片级的画面,要写清楚光线方向、皮肤材质、镜头参数这些具体信息,而不是堆形容词。
体验到了大模型的能力和应用。 只需要几句话就能生成一张图片,同时还有其他许多模型能给我们带来不同的帮助。
3.GitHub个人主页的搭建
作业给了两个方案,而我采取了第一个方案。之后我会再尝试用第二个方案来充实个人主页。
我的Github个人主页链接:https://github.com/Ki-K4

4.博客园随笔
https://www.cnblogs.com/asdllx/p/22935169
5.博文编辑页面截图

浙公网安备 33010602011771号