软件工程第一次作业

软件工程第一次作业

这个作业属于哪个课程 202601 软件工程
这个作业要求在哪里 第一次作业要求
这个作业的目标 完成 GitHub、博客园与 Hugging Face 账号准备;使用 Flux API 结合前端界面完成一次真实图像生成;搭建 GitHub 个人主页并梳理技能树与未来规划
学号 162404103

一、准备工作

  • GitHub 账号popochus,头像、昵称、个人资料都已完善。
  • 博客园账号caipo,已开通博客,编辑器已切换为 Markdown,并完成实名制。
  • 已关注任课老师吴越钟,助教张明圣王奇蕊,并加入博客园班级(202601 软件工程)。

二、Hugging Face API 调用实验报告

2.1 注册账号、获取 Token

  1. 打开 Hugging Face 官网 注册账号;
  2. 进入 Settings → Access Tokens
  3. Create new token,权限选 Read
  4. 把生成的 hf_ 开头的 Token 复制保存好。

2.2 代码实现

模型用的是作业要求的 XLabs-AI/flux-RealismLora,前端用 Python + Gradio 写。Gradio 是 Python 的一个库,几行代码就能生成一个能点的网页界面。

两条调用通道

动手之后才发现,这个模型生成图片需要消耗额度。所以代码里写了两种调用方式,界面上可以切换:

通道 原理 特点
免费通道(最后实际用的) gradio_client 调社区里公开部署的一个在线空间,空间内部加载的就是 XLabs-AI/flux-RealismLora 不花推理额度,免费
HF 官方 API huggingface_hubInferenceClient 调用 作业原本要求的方式,但这个模型只支持付费提供商

社区空间 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 运行环境与步骤

  1. VS Code 打开项目文件夹,在项目里建一个 Python 虚拟环境 .venv 并激活;
  2. 安装依赖:python -m pip install gradio huggingface_hub gradio_client pillow
  3. 运行 web_app.py,终端显示 http://127.0.0.1:7860,浏览器自动打开;
  4. 在界面里填自己的 hf_ Token,输入提示词,点「生成图像」;
  5. 右边显示生成的图,下面「API 调用记录」显示完整日志。

上面这几步对应下面两张截图:

第一次生成(不完善版本)

0dbca48df3794f3afa8a72260e525d5e

第二次生成(完善版本)

69a4b61afdc82a8a57501f5981f91a30

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

  • 现象:按作业原本的要求用 InferenceClientXLabs-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 的原因。

第一次生成:

image

第二次生成(完善) —— 按上面几个方面逐条写清楚,改成全英文:

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.

结果:明显是照片级写实 —— 老人脸上的皱纹、手上凸起的青筋、握着刻刀的动作都看得清,背景木垛的虚化也很自然。

第二次生成,即最终生成的图像:

image

两次对比:第二版把"画谁、在做什么、什么环境、什么镜头、什么质感、不要什么"一项一项全写清楚了,而且统一成英文,出来的效果从奇幻插画变成了照片级写实。同一句"一位老人在工作",第一版给不出任何可用信息,第二版把每个细节都交代到位。

API 调用成功的记录:

image

image

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 提交上去,它就会自动显示在主页。

image

四、个人技术随笔

image


五、本博文编辑页面截图

image

posted @ 2026-09-10 17:29  caipo  阅读(8)  评论(0)    收藏  举报