软件工程第一次作业:记一次调用API和前后端搭建

这个作业属于哪个课程 H202601软件工程与软件工程实践
这个作业要求在哪里 2026秋软件工程个人作业(第一次)
这个作业的目标 梳理个人技能树并自我评估,统计代码量,明确课程学习预期;理解前后端与API请求调用原理,掌握Markdown写作;使用AI工具生成软件工程学习指南并做合理性分析,同时完成博客园Markdown博文发布
学号 102401607

一、huggingface-API的调用

作业要求在调用模型XLabs-AI/flux-RealismLora的代码基础上结合前端接口,实现交互生成。
由于该模型部署在国外,因此运行全程需保持代理状态开启。

1.前端代码

<!DOCTYPE html>
<!-- 网页文档声明,使用HTML5标准 -->
<html lang="zh-CN">
<!-- 设置网页语言为中文 -->
<head>
    <!-- 设置网页字符编码为UTF-8,防止中文乱码 -->
    <meta charset="UTF-8">
    <!-- 网页标题,浏览器标签栏显示 -->
    <title>Flux RealismLora 写实图像生成器</title>
    <style>
        /* 全局样式重置,统一所有元素盒模型 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        /* 页面主体样式,背景渐变、基础字体、文字颜色 */
        body {
            font-family: "Microsoft YaHei", Arial, sans-serif;
            background:
                radial-gradient(circle at top left, rgba(74, 144, 226, 0.14), transparent 35%),
                linear-gradient(135deg, #f0f6ff 0%, #f7faff 45%, #eaf4ff 100%);
            color: #2c3e50;
            min-height: 100vh;
        }

        /* 顶部标题栏样式,蓝渐变背景,阴影效果 */
        header {
            background: linear-gradient(90deg, #1e69c4 0%, #4a90e2 100%);
            color: #ffffff;
            padding: 40px 60px;
            box-shadow: 0 4px 24px rgba(30, 105, 196, 0.18);
        }

        /* 标题文字样式 */
        header h1 {
            font-size: 30px;
            font-weight: 700;
            letter-spacing: 1px;
        }

        /* 标题下方描述文字 */
        header p {
            margin-top: 12px;
            font-size: 15px;
            opacity: 0.92;
        }

        /* 主体容器,使用网格布局分为左右两栏 */
        .main-wrapper {
            width: 94%;
            max-width: 1480px;
            margin: 40px auto;
            display: grid;
            grid-template-columns: 0.85fr 1.85fr;
            gap: 40px;
            align-items: stretch;
        }

        /* 左侧面板:提示词输入区域 */
        .left-panel {
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
            padding: 20px 10px;
        }

        /* 左侧面板小标题 */
        .left-panel h2 {
            font-size: 22px;
            font-weight: 600;
            color: #1e69c4;
            margin-bottom: 20px;
        }

        /* 提示词输入文本框样式 */
        textarea {
            width: 100%;
            height: 240px;
            padding: 18px;
            border: 1px solid #dce6f5;
            border-radius: 14px;
            font-size: 16px;
            line-height: 1.8;
            resize: none;
            outline: none;
            background: #ffffff;
            box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.04);
            transition: border-color 0.3s ease, box-shadow 0.3s ease;
        }

        /* 输入框获得焦点(点击)时的高亮效果 */
        textarea:focus {
            border-color: #4a90e2;
            box-shadow: 0 0 0 4px rgba(74, 144, 226, 0.12);
        }

        /* 生成按钮基础样式 */
        .btn {
            margin-top: 24px;
            padding: 16px 36px;
            background: linear-gradient(90deg, #1e69c4 0%, #4a90e2 100%);
            color: #ffffff;
            border: none;
            border-radius: 14px;
            font-size: 17px;
            font-weight: 600;
            cursor: pointer;
            transition: transform 0.2s ease, box-shadow 0.2s ease;
        }

        /* 鼠标悬浮按钮上浮效果 */
        .btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 12px 28px rgba(30, 105, 196, 0.35);
        }

        /* 按钮点击按下效果 */
        .btn:active {
            transform: translateY(0);
        }

        /* 使用提示信息盒子 */
        .info-box {
            margin-top: 24px;
            padding: 18px 22px;
            background: linear-gradient(135deg, #f0f6ff 0%, #eaf4ff 100%);
            border-left: 4px solid #4a90e2;
            border-radius: 14px;
            box-shadow: 0 4px 16px rgba(74, 144, 226, 0.08);
        }

        /* 提示框标题 */
        .info-box h3 {
            font-size: 16px;
            font-weight: 700;
            color: #1e69c4;
            margin-bottom: 12px;
        }

        /* 无序列表样式 */
        .info-box ul {
            padding-left: 20px;
            margin: 0;
        }

        /* 列表条目 */
        .info-box li {
            font-size: 14px;
            line-height: 1.8;
            color: #34495e;
            margin-bottom: 8px;
        }

        /* 最后一条列表取消下边距 */
        .info-box li:last-child {
            margin-bottom: 0;
        }

        /* 右侧面板:图片展示区域 */
        .right-panel {
            background: #ffffff;
            border-radius: 18px;
            box-shadow: 0 10px 34px rgba(0, 0, 0, 0.08);
            padding: 36px;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 560px;
            position: relative;
            overflow: hidden;
        }

        /* 右侧面板顶部蓝色装饰条 */
        .right-panel::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 6px;
            background: linear-gradient(90deg, #1e69c4, #4a90e2);
        }

        /* 图片结果容器 */
        #result {
            width: 100%;
            text-align: center;
            position: relative;
            z-index: 1;
        }

        /* 生成图片的样式 */
        #result img {
            max-width: 100%;
            border-radius: 14px;
            box-shadow: 0 8px 28px rgba(0, 0, 0, 0.14);
        }

        /* 占位提示文字样式,未生成图片时显示 */
        .placeholder {
            color: #7f8c8d;
            font-size: 17px;
            padding: 80px 40px;
            background:
                radial-gradient(circle at center, rgba(74, 144, 226, 0.08), transparent 48%);
            border-radius: 14px;
        }

        /* 响应式适配:屏幕宽度小于992px时,左右栏改为上下单列 */
        @media (max-width: 992px) {
            .main-wrapper {
                grid-template-columns: 1fr;
            }

            header {
                padding: 30px 24px;
            }

            .right-panel {
                min-height: 420px;
            }
        }
    </style>
</head>
<body>
    <!-- 页面头部标题区域 -->
    <header>
        <h1>Flux RealismLora 写实图像生成器</h1>
        <p>输入英文提示词,生成真实照片风格图像</p>
    </header>

    <!-- 主体左右分栏容器 -->
    <div class="main-wrapper">
        <!-- 左侧:提示词输入面板 -->
        <div class="left-panel">
            <h2>输入提示词</h2>
            <!-- 提示词输入框,附带示例占位文字 -->
            <textarea id="promptInput" placeholder="例如:photorealistic photograph, coastal seaside at golden hour, soft warm sunset light, natural shallow depth of field, real-world texture, sharp details, 8k, shot on DSLR camera"></textarea>
            <!-- 点击触发图片生成函数 -->
            <button class="btn" onclick="onGenerate()">生成图片</button>

            <!-- 使用说明提示框 -->
            <div class="info-box">
                <h3>使用提示</h3>
                <ul>
                    <li>建议使用英文提示词,生成效果更稳定。</li>
                    <li>可以补充场景、光线、相机和画质细节。</li>
                    <li>生成需要 20–60 秒,请耐心等待结果。</li>
                </ul>
            </div>
        </div>

        <!-- 右侧:图片展示面板 -->
        <div class="right-panel">
            <div id="result">
                <!-- 默认占位提示文本 -->
                <p class="placeholder">生成的图片将在此处显示</p>
            </div>
        </div>
    </div>

<script>
// 点击生成按钮触发的异步请求函数
async function onGenerate(){
    // 获取输入框内的提示词
    const prompt = document.getElementById("promptInput").value;
    // 获取图片结果展示容器
    const resDiv = document.getElementById("result");

    // 判断提示词是否为空
    if (!prompt.trim()) {
        resDiv.innerHTML = `<p class="placeholder">请先输入提示词</p>`;
        return;
    }

    // 修改页面提示:正在请求后端生成图片
    resDiv.innerHTML = `<p class="placeholder">正在请求API生成图片,请等待...</p>`;

    try{
        // 向后端 /generate 接口发送POST请求
        const resp = await fetch("/generate",{
            method:"POST",
            headers:{"Content-Type":"application/json"},
            body:JSON.stringify({prompt:prompt})
        })

        // 解析后端返回的JSON数据
        const json = await resp.json();

        // 判断是否返回图片地址,成功则展示图片;失败展示错误信息
        if(json.image_url){
            resDiv.innerHTML = `<img src="${json.image_url}" alt="生成结果">`;
        }else{
            resDiv.innerHTML = `<p class="placeholder">错误:${json.error}</p>`;
        }

    }catch(err){
        // 捕获网络层面异常,展示网络报错
        resDiv.innerHTML = `<p class="placeholder">网络异常:${err}</p>`;
    }
}
</script>
</body>
</html>

2.后端代码

from flask import Flask, request, jsonify, render_template, send_from_directory
from flask_cors import CORS
import os
import uuid
import warnings
from datetime import datetime
from PIL import Image
from huggingface_hub import InferenceClient

# 忽略无关警告信息
warnings.filterwarnings("ignore")
# 创建Flask后端应用实例
app = Flask(__name__)
# 开启跨域,允许前端页面访问后端接口
CORS(app)

# 定义图片输出保存目录
OUTPUT_FOLDER = os.path.join(os.path.dirname(__file__), "static", "output")
# 如果文件夹不存在则自动创建,存在则不报错
os.makedirs(OUTPUT_FOLDER, exist_ok=True)

# HuggingFace的访问令牌,自行填写
HF_TOKEN = ""
# 使用的AI图像生成模型ID
HF_MODEL_ID = "XLabs-AI/flux-RealismLora"
# 提示词最大字符长度限制
MAX_PROMPT_LEN = 600

# 初始化HuggingFace推理客户端
client = InferenceClient(api_key=HF_TOKEN)

# 路由:提供生成图片的静态资源访问
@app.route('/static/output/<filename>')
def serve_output_img(filename):
    return send_from_directory(OUTPUT_FOLDER, filename)

# 首页路由,返回前端网页
@app.route("/")
def index():
    return render_template("index.html")


# 图片生成接口,POST请求
@app.route("/generate", methods=["POST"])
def generate_image():
    try:
        # 获取前端传来的JSON数据
        data = request.get_json()
        # 取出提示词prompt
        prompt = data.get("prompt", "")
        # 去除首尾空格,并截断到最大长度
        prompt = prompt.strip()[:MAX_PROMPT_LEN]

        # 控制台打印日志,记录请求时间与提示词
        print(f"\n[{datetime.now().strftime('%H:%M:%S')}] 收到生成请求")
        print(f"prompt: {prompt[:100]}")

        # 调用HuggingFace接口,文本生成图片
        image = client.text_to_image(
            prompt=prompt,
            model=HF_MODEL_ID,
            height=512,
            width=512,
            num_inference_steps=30,
            guidance_scale=7.5
        )
        # 使用uuid生成唯一文件名,防止重名覆盖
        filename = f"{uuid.uuid4()}.png"
        # 拼接完整保存路径
        save_path = os.path.join(OUTPUT_FOLDER, filename)
        # 将图片保存到本地磁盘
        image.save(save_path, format="PNG")
        # 组装图片可访问的url地址,返回给前端
        image_url = f"/static/output/{filename}"
        print(f"✅图片已保存本地:{save_path}")

        # 返回JSON结果给前端,携带图片地址
        return jsonify({"image_url": image_url})

    except Exception as e:
        # 捕获所有异常,打印错误信息
        err_msg = str(e)
        print(f"❌异常:{err_msg}")
        # 返回错误信息和500状态码
        return jsonify({"error": str(e)}), 500


# 程序入口,直接运行此文件则启动后端服务
if __name__ == "__main__":
    # 在本机5000端口启动,debug模式开启
    app.run(host="127.0.0.1", port=5000, debug=True)

3.主要错误及解决过程

3.1 传输超时代理直接掐断传输通道导致测试连通性的代码出现如下图的调用失败,后续通过配置代理环境切换节点完成。
超时报错
3.2 图片过大导致前端如下图无法显示图片
前端无法显示
后采取生成图片保存至本地,前端调用URL显示的方法成功在前端展示出最终图片
本地保存及路径
最终图片生成

4.提示词设计

由于该模型只有三次免费额度,且在第一次额度因为后端没有设计本地保存路径和前端无法展示导致浪费后,生成提示词力求完整完善,完整提示词如下

masterpiece, best quality, ultra realistic photograph, 1 young woman, natural makeup, soft facial features, clear skin texture, slight smile, looking at camera, casual white shirt, standing by a sunlit window, warm morning light, soft shadow, shallow depth of field, portrait photography, 85mm lens, f1.8, natural skin tone, high detail, photorealistic, no overexposure

最终调用成功的后端截图保存于本地的生成效果原图
调用成功后端

结果图

5.总结

  1. 调用API首先要获取Token,且应当保存好。
  2. 若模型部署在国外,应当保证代理环境的持续稳定,并配置好相应的环境。
  3. 要考虑好生成图片的展示方式,可以避免不必要的浪费。
  4. 该API调用所在文件层次为
  • model1
    • server.py(后端)
    • text_proxy.py(测试连通性)
    • templates
      • index.html(前端)
    • static(此文件夹会在后端代码运行后自动生成)
      • output(保存图片)

二、Github个人主页搭建

在创建与个人用户名同名的仓库后在下完成个人主页建设
Github个人主页建设

三、博客园自我介绍随笔

课程学习随笔

四、该条博客后台编辑截图

屏幕截图 2026-09-09 220521

posted @ 2026-09-09 22:06  空城空旧忆丶  阅读(4)  评论(0)    收藏  举报