软件工程第一次作业
软件工程第一次作业
| 这个作业属于哪个课程 | 202601 软件工程 |
|---|---|
| 这个作业要求在哪里 | 第一次作业要求 |
| 这个作业的目标 | 完成 GitHub、博客园与 Hugging Face 账号准备;使用 Flux API 结合前端界面完成一次真实图像生成;搭建 GitHub 个人主页并梳理技能树与未来规划 |
| 学号 | 162404103 |
一、准备工作
- GitHub 账号:
popochus,头像、昵称、个人资料都已完善。 - 博客园账号:
caipo,已开通博客,编辑器已切换为 Markdown,并完成实名制。 - 已关注任课老师吴越钟,助教张明圣、王奇蕊,并加入博客园班级(202601 软件工程)。
二、Hugging Face API 调用实验报告
2.1 注册账号、获取 Token
- 打开 Hugging Face 官网 注册账号;
- 进入
Settings → Access Tokens; - 点
Create new token,权限选Read; - 把生成的
hf_开头的 Token 复制保存好。
2.2 代码实现
模型用的是作业要求的 XLabs-AI/flux-RealismLora,前端用 Python + Gradio 写。Gradio 是 Python 的一个库,几行代码就能生成一个能点的网页界面。
两条调用通道
动手之后才发现,这个模型生成图片需要消耗额度。所以代码里写了两种调用方式,界面上可以切换:
| 通道 | 原理 | 特点 |
|---|---|---|
| 免费通道(最后实际用的) | 用 gradio_client 调社区里公开部署的一个在线空间,空间内部加载的就是 XLabs-AI/flux-RealismLora |
不花推理额度,免费 |
| HF 官方 API | 用 huggingface_hub 的 InferenceClient 调用 |
作业原本要求的方式,但这个模型只支持付费提供商 |
社区空间 DamarJati/FLUX.1-RealismLora 的源码里,模型是这样加载的,可以确认它就是作业要求的那个:
base_model = "black-forest-labs/FLUX.1-dev"
pipe = DiffusionPipeline.from_pretrained(base_model, torch_dtype=torch.bfloat16)
lora_repo = "XLabs-AI/flux-RealismLora"
pipe.load_lora_weights(lora_repo)
后端核心代码
下面是 web_app.py 里后端主要部分:
import os
import time
import datetime
import gradio as gr
import huggingface_hub
from huggingface_hub import InferenceClient
from gradio_client import Client
from PIL import Image
# ============ 模型配置 ============
MODEL = "XLabs-AI/flux-RealismLora" # 作业要求的模型
FALLBACK_MODEL = "black-forest-labs/FLUX.1-schnell" # 官方 API 额度不足时的备选
# ============ 免费通道:社区公开空间 ============
# 该空间内部加载的就是 XLabs-AI/flux-RealismLora,可以免费调用
FREE_SPACE = "DamarJati/FLUX.1-RealismLora"
MODE_FREE = "🎁 免费通道 · 社区空间(推荐)"
MODE_API = "🔑 HF 官方 API(作业原始方式)"
MAX_RETRIES = 4 # 网络抖动时的自动重试次数
OUTPUT_DIR = "生成结果" # 生成的图片同时保存到这个文件夹
_space_client = None # 缓存的连接对象,避免每次重复连接
def _log(log_lines, text=""):
"""同时写入「网页日志面板」和「VS Code 终端」,方便截图取证"""
log_lines.append(text)
print(text, flush=True)
def _get_space_client(token):
"""连接(或复用)免费通道的社区空间"""
global _space_client
if _space_client is None:
_space_client = Client(
FREE_SPACE,
token=token if token and token.startswith("hf_") else None,
verbose=False,
)
return _space_client
def _save_image(image):
"""把生成的图片保存到本地 生成结果/ 文件夹,方便截图和留存"""
os.makedirs(OUTPUT_DIR, exist_ok=True)
name = datetime.datetime.now().strftime("flux_%Y%m%d_%H%M%S.png")
path = os.path.join(OUTPUT_DIR, name)
image.convert("RGB").save(path)
return path
def generate_via_free_space(prompt, steps, cfg_scale, lora_scale, width, height, seed, token, log_lines):
"""【方式一】通过社区公开空间调用 flux-RealismLora(免费)"""
_log(log_lines, f" 免费通道空间:{FREE_SPACE}")
_log(log_lines, f" 空间内实际加载的模型:{MODEL} + black-forest-labs/FLUX.1-dev")
# 空间源码里有一句 `i % (steps // 10)`,steps 小于 10 会除零报错
if steps < 10:
_log(log_lines, " ⚠️ 步数小于 10 会导致空间内部除零报错,已自动调整为 10")
steps = 10
for attempt in range(1, MAX_RETRIES + 1):
try:
_log(log_lines, f" 第 {attempt} 次请求(免费通道,首次调用可能需冷启动)...")
client = _get_space_client(token)
result = client.predict(
prompt,
float(cfg_scale),
int(steps),
False, # Randomize seed:关闭,保证结果可复现
int(seed), # Seed
int(width),
int(height),
float(lora_scale),
api_name="/run_lora",
)
# 返回值是 (图片路径, 实际使用的 seed)
image_path = result[0] if isinstance(result, (list, tuple)) else result
used_seed = result[1] if isinstance(result, (list, tuple)) and len(result) > 1 else seed
image = Image.open(image_path).convert("RGB")
saved = _save_image(image)
_log(log_lines, "✅ 生成成功!")
_log(log_lines, f" 图片尺寸:{image.size[0]} x {image.size[1]} 像素")
_log(log_lines, f" 实际 seed:{used_seed} LoRA 强度:{lora_scale}")
_log(log_lines, f" 图片已保存:{saved}")
return image, "\n".join(log_lines)
except Exception as e:
msg = f"{type(e).__name__}: {e}"
_log(log_lines, f" ❌ 第 {attempt} 次尝试失败:{msg}")
_space_client = None # 连接可能已失效,下次重新建立
# ZeroGPU 配额用尽:重试没有意义,必须换成带 Token 的调用
if "ZeroGPU" in msg or "GPU quota" in msg or "runs limit" in msg:
_log(log_lines, "")
_log(log_lines, " 💡 免费 GPU 配额用尽:匿名调用有共享次数限制")
_log(log_lines, " 请在「Hugging Face Token」框里填入你自己的 hf_ 开头 Token 后重新生成")
_log(log_lines, " 填入 Token 后会使用你账号的独立配额(免费账号每天都有额度)")
_log(log_lines, " Token 在 https://huggingface.co/settings/tokens 获取")
return None, "\n".join(log_lines)
if attempt < MAX_RETRIES:
wait = attempt * 5
_log(log_lines, f" ⏳ 等待 {wait} 秒后自动重试...")
time.sleep(wait)
_log(log_lines, "")
_log(log_lines, "免费通道失败常见原因:")
_log(log_lines, " 1. Token 未填写或已失效 → 填上有效 Token,配额更充足")
_log(log_lines, " 2. 网络不稳定 → 检查网络是否正常,稍后重试")
_log(log_lines, " 3. 空间排队或重启中 → 打开 https://huggingface.co/spaces/"
f"{FREE_SPACE} 看能不能正常访问")
return None, "\n".join(log_lines)
def generate_via_official_api(token, prompt, provider, model, log_lines):
"""【方式二】Hugging Face 官方 Inference API(作业原始要求的方式)"""
_log(log_lines, f" huggingface_hub 版本:{huggingface_hub.__version__}")
if not token or not token.startswith("hf_"):
_log(log_lines, "❌ 错误:HF 官方 API 需要以 hf_ 开头的 Token,请先填写")
return None, "\n".join(log_lines)
# 新版 huggingface_hub(>=1.0):provider 参数在 InferenceClient() 构造函数里
# 旧版 huggingface_hub(0.29~0.x):provider 参数在 text_to_image() 方法里
try:
client = InferenceClient(provider=provider, token=token)
provider_in_constructor = True
_log(log_lines, f" InferenceClient 已创建(provider={provider})")
except TypeError:
client = InferenceClient(token=token)
provider_in_constructor = False
_log(log_lines, " ⚠️ 构造时不支持 provider,改用方法参数传入")
for attempt in range(1, MAX_RETRIES + 1):
try:
_log(log_lines, f" 第 {attempt} 次请求 Hugging Face 服务器(provider={provider})...")
if provider_in_constructor:
image = client.text_to_image(model=model, prompt=prompt)
else:
image = client.text_to_image(model=model, prompt=prompt, provider=provider)
saved = _save_image(image)
_log(log_lines, "✅ 生成成功!")
_log(log_lines, f" 图片尺寸:{image.size[0]} x {image.size[1]} 像素")
_log(log_lines, f" 图片已保存:{saved}")
return image, "\n".join(log_lines)
except Exception as e:
msg = str(e)
_log(log_lines, f" ❌ 第 {attempt} 次尝试失败:{msg}")
# 402 = 额度不足,重试没有意义
if "402" in msg:
_log(log_lines, " 💡 402 = Hugging Face 免费额度已用尽")
_log(log_lines, " 官方免费账号每月仅 $0.10 额度,且该 LoRA 只支持付费提供商")
_log(log_lines, f" → 建议改用「{MODE_FREE}」,或用备选模型 {FALLBACK_MODEL}")
break
if attempt < MAX_RETRIES:
wait = attempt * 5
_log(log_lines, f" ⏳ 等待 {wait} 秒后自动重试...")
time.sleep(wait)
_log(log_lines, "")
_log(log_lines, "失败常见原因:")
_log(log_lines, f" 1. 402 额度不足 → 改用免费通道,或把模型换成 {FALLBACK_MODEL}")
_log(log_lines, " 2. 网络不稳定(连接被掐断)→ 检查网络是否正常")
_log(log_lines, " 3. Token 错误或过期 → 重新生成 Token")
return None, "\n".join(log_lines)
def generate_image(mode, token, prompt, provider, model,
steps, cfg_scale, lora_scale, width, height, seed):
"""统一入口:根据选择的调用方式分发"""
log_lines = []
_log(log_lines, f"[{datetime.datetime.now().strftime('%H:%M:%S')}] 开始调用...")
_log(log_lines, f" 调用方式:{mode}")
_log(log_lines, f" 模型:{MODEL}")
_log(log_lines, f" 提示词:{prompt[:80]}..." if len(prompt) > 80 else f" 提示词:{prompt}")
if not prompt.strip():
_log(log_lines, "❌ 错误:提示词不能为空")
return None, "\n".join(log_lines)
if mode == MODE_FREE:
return generate_via_free_space(
prompt, steps, cfg_scale, lora_scale, width, height, seed, token, log_lines
)
else:
return generate_via_official_api(token, prompt, provider, model, log_lines)
2.3 运行环境与步骤
- 用 VS Code 打开项目文件夹,在项目里建一个 Python 虚拟环境
.venv并激活; - 安装依赖:
python -m pip install gradio huggingface_hub gradio_client pillow; - 运行
web_app.py,终端显示http://127.0.0.1:7860,浏览器自动打开; - 在界面里填自己的
hf_Token,输入提示词,点「生成图像」; - 右边显示生成的图,下面「API 调用记录」显示完整日志。
上面这几步对应下面两张截图:
第一次生成(不完善版本)
第二次生成(完善版本)
2.4 过程中遇到的两个问题
这两个问题都不是代码写错,而是网络和额度的问题。
问题一:调用时 SSL 握手超时,连接被掐断
- 现象:调用过程中偶尔报
ConnectTimeout: _ssl.c:993: The handshake operation timed out,也就是 SSL 握手超时,连接被切断(截图里能看到,第 1 次请求就是这样失败的); - 原因:本机网络到 Hugging Face 的连接不稳定,握手阶段就超时了,请求还没发出去就被掐断;
- 解决:在代码里加了自动重试——失败后等 5 秒、10 秒、15 秒再试,第 2 次就成功了(截图日志里的"等待 5 秒后自动重试"就是这段)。
- 说明:这属于外部网络环境问题,跟代码逻辑无关,这里不再展开。
问题二:官方 API 返回 402 Payment Required
- 现象:按作业原本的要求用
InferenceClient调XLabs-AI/flux-RealismLora,直接返回402 Payment Required; - 原因:查资料后确认,这个模型在 Hugging Face 上只支持 fal-ai / replicate / wavespeed 三家付费推理提供商,官方的免费推理服务器不提供它;免费账号每月只有 $0.10 额度,用完就返回 402;
- 解决:官方这条路免费走不通,所以改用社区公开空间
DamarJati/FLUX.1-RealismLora。这个空间内部加载的正是作业要求的同一个模型(配 FLUX.1-dev 底座),可以免费调用。最后出图用的都是这条免费通道,模型本身跟作业要求是一致的。
发现:模型开源不等于调用免费。模型是公开的,但跑模型用的算力要花钱;免费额度也很有限,用完就调不动了。
2.5 提示词的设计与修改过程
这部分想做的事情,是生成一张尽量像真人的照片,主题一直是一位正在工作的老人。提示词前后改了一版,出来的效果差别很大。
第二次的提示词,就是按下面这几个方面写的:
| 要素 | 第二次提示词里对应的部分 | 作用 |
|---|---|---|
| 风格定位 | Photorealistic documentary-style | 定整体画面风格 |
| 主体与动作 | an elderly Chinese craftsman working attentively;weathered hands carving a piece of wood with a chisel | 明确画谁、在做什么 |
| 环境与道具 | in his small workshop;a piece of wood、a chisel | 增加真实感和生活气息 |
| 镜头与景深 | portrait lens, shallow depth of field, creamy bokeh | 让画面有摄影感 |
| 材质与质感 | authentic natural skin texture, film grain | 提升细节和真实度 |
| 负面排除 | no text, no watermark | 避免生成乱码文字 |
第一次生成(不完善) —— 提示词只有一句话,中英文混着写:
一位老人在工作,画面真实,highly detailed. 摄影风格
结果跟想要的完全不是一回事 —— 出来的是一张奇幻插画:巨大的石像怪兽、前面站着一个人物,整体像是动漫风格,跟要求差得很远。
问题出在:
- 提示词太短,一句话里只说了"画什么",没说清楚"怎么画",剩下的全交给模型自由发挥;
- "画面真实""摄影风格"这种说法太笼统,模型不知道要什么镜头、什么景深、什么皮肤质感;
- 没有排除文字,画面两侧的柱子上出现了模型自己编出来的"字",看着像汉字但认不出来 —— 这也是后面在提示词里加
no text, no watermark的原因。
第一次生成:

第二次生成(完善) —— 按上面几个方面逐条写清楚,改成全英文:
Photorealistic documentary-style portrait of an elderly Chinese craftsman working attentively in his small workshop, weathered hands carving a piece of wood with a chisel, portrait lens, shallow depth of field, creamy bokeh, authentic natural skin texture, film grain, no text, no watermark.
结果:明显是照片级写实 —— 老人脸上的皱纹、手上凸起的青筋、握着刻刀的动作都看得清,背景木垛的虚化也很自然。
第二次生成,即最终生成的图像:

两次对比:第二版把"画谁、在做什么、什么环境、什么镜头、什么质感、不要什么"一项一项全写清楚了,而且统一成英文,出来的效果从奇幻插画变成了照片级写实。同一句"一位老人在工作",第一版给不出任何可用信息,第二版把每个细节都交代到位。
API 调用成功的记录:
2.6 API 调用的体验与心得(实验总结)
API 调用体验
了解了 InferenceClient 的用法,几行代码就能调文生图模型;配上 Gradio 可以很快搭出能交互的前端,不用写前后端代码,适合快速验证想法。不过这次也说明,接口能不能调通不只看代码写没写对——模型的可用提供商、账号额度、网络环境都会卡住你,这些都要自己去试、去查。
技术收获
- 学会在 VS Code 里建虚拟环境、用
pip装依赖、运行 Python 程序; - 了解了
huggingface_hub调 Inference API 的基本方式,以及不同版本参数位置的差异; - 学会用 Gradio 搭一个带输入、输出和日志面板的界面;
- 理解了 LoRA 是什么;
- 知道了 402 这类报错代表什么,以及怎么绕过去。
提示词方面的体会
提示词写得好不好直接决定效果,具体、有结构的描述明显更好:
- 全英文比中英文混着写更稳定;
- 光说"一位老人在工作""画面真实"没用,得把人物、动作、道具一项一项写出来;
- 镜头和景深参数(人像镜头、浅景深、奶油般虚化)是照片级写实的关键;
- 材质描述(真实皮肤纹理、胶片颗粒)能明显提升真实感;
- 最后一定要写
no text, no watermark,不然画面里很容易冒出乱码文字。
三、GitHub 个人主页搭建
用的是个人资料自述文件方案:建 popochus/popochus 仓库,把 README.md 提交上去,它就会自动显示在主页。








浙公网安备 33010602011771号