第一次作业

这个作业属于哪个课程 202601软件工程
这个作业要求在哪里 2026秋软件工程个人作业(第一次)
这个作业的目标 huggingface-API的调用 、Github个人主页搭建、梳理当前的技能树和技术偏好,并进行自我评估
学号 102401104

一、准备工作

  • GitHub:账号为astronomy-yy
  • 博客园:账号为astronomyy,已完善各项信息并加入班级、关注老师博客

二、huggingface-API的调用

2.1 api调用实现

(1)整体架构:浏览器前端 (index.html) → Flask 后端 (app.py) → Hugging Face API → Flux 模型

(2)技术栈

  • 后端:Python + Flask
  • 前端:HTML + CSS + 原生 JavaScript
  • AI 模型:Hugging Face Inference API

(3)核心流程

  • 用户在前端输入提示词,点击 "生成图像"
  • 前端通过 fetch 把提示词 POST 到后端 /api/generate 接口
  • 后端调用 Hugging Face InferenceClient.text_to_image() 生成图片
  • 后端把图片转成 base64 返回给前端
  • 前端显示生成的图片,支持下载

(4)核心代码

后端代码:

import os
import base64
import time
from io import BytesIO
from flask import Flask, request, jsonify, send_from_directory
from huggingface_hub import InferenceClient
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

app = Flask(__name__, static_folder="static")


def load_env(env_path=".env"):
    """读取 .env 文件到环境变量(不依赖 python-dotenv)"""
    if not os.path.exists(env_path):
        return
    with open(env_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" not in line:
                continue
            key, _, value = line.partition("=")
            key = key.strip()
            value = value.strip()
            # 只设置未设置的环境变量,不覆盖已有配置
            if key not in os.environ:
                os.environ[key] = value


# 加载配置
load_env()

# ========== 从环境变量读取配置 ==========
MODEL_ID = os.environ.get("MODEL_ID", "black-forest-labs/FLUX.1-schnell")
HF_TOKEN = os.environ.get("HF_TOKEN", "")

if not HF_TOKEN:
    raise ValueError("未找到 HF_TOKEN,请在 .env 文件中配置你的 Hugging Face Token")

# 设置代理环境变量(InferenceClient 底层 httpx 会自动读取)
os.environ.setdefault("HTTP_PROXY", "http://127.0.0.1:7892")
os.environ.setdefault("HTTPS_PROXY", "http://127.0.0.1:7892")
# ========================================

# 初始化 InferenceClient
client = InferenceClient(api_key=HF_TOKEN)


@app.route("/")
def index():
    return send_from_directory("static", "index.html")


@app.route("/api/generate", methods=["POST"])
def generate_image():
    try:
        data = request.get_json()
        prompt = data.get("prompt", "").strip()

        if not prompt:
            return jsonify({"error": "提示词不能为空"}), 400

        print(f"调用模型:{MODEL_ID},提示词:{prompt}")

        # 带重试的图片生成
        max_retries = 3
        image = None
        last_error = None

        for attempt in range(max_retries):
            try:
                image = client.text_to_image(
                    prompt,
                    model=MODEL_ID,
                    num_inference_steps=4,
                    guidance_scale=3.5,
                    width=1024,
                    height=1024,
                )
                break
            except Exception as e:
                last_error = str(e)
                print(f"第 {attempt + 1} 次尝试失败: {last_error}")
                if attempt < max_retries - 1:
                    time.sleep(2)

        if image is None:
            return jsonify({"error": f"生成失败(重试{max_retries}次后仍失败):{last_error}"}), 500

        # 转换为 base64
        buf = BytesIO()
        image.save(buf, format="PNG")
        img_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")

        print("图像生成成功")
        return jsonify({
            "success": True,
            "image": f"data:image/png;base64,{img_b64}"
        })

    except Exception as e:
        print(f"生成失败:{str(e)}")
        return jsonify({"error": f"生成失败:{str(e)}"}), 500


if __name__ == "__main__":
    os.makedirs("static", exist_ok=True)
    print(f"Flask 服务启动中... 模型: {MODEL_ID}")
    print("访问 http://localhost:5000 使用")
    app.run(host="0.0.0.0", port=5000, debug=False)

前端代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flux 写实图像生成器</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
        }

        body {
            min-height: 100vh;
            padding: 40px 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            display: flex;
            align-items: flex-start;
            justify-content: center;
        }

        .container {
            width: 100%;
            max-width: 880px;
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
            padding: 36px 40px;
        }

        h1 {
            color: #1f2937;
            text-align: center;
            font-size: 28px;
            font-weight: 600;
            margin-bottom: 8px;
        }

        .subtitle {
            text-align: center;
            color: #6b7280;
            font-size: 14px;
            margin-bottom: 28px;
        }

        /* 示例提示词 */
        .prompt-examples {
            margin-bottom: 16px;
        }

        .prompt-examples label {
            font-size: 14px;
            color: #374151;
            font-weight: 500;
            display: block;
            margin-bottom: 8px;
        }

        .example-tags {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-bottom: 12px;
        }

        .example-tag {
            padding: 6px 14px;
            background: #f3f4f6;
            border: 1px solid #e5e7eb;
            border-radius: 20px;
            font-size: 13px;
            color: #4b5563;
            cursor: pointer;
            transition: all 0.2s;
        }

        .example-tag:hover {
            background: #e0e7ff;
            border-color: #6366f1;
            color: #4338ca;
        }

        /* 输入区域 */
        .input-section {
            margin-bottom: 20px;
        }

        textarea {
            width: 100%;
            min-height: 110px;
            padding: 14px 16px;
            border: 1px solid #d1d5db;
            border-radius: 12px;
            font-size: 15px;
            line-height: 1.6;
            resize: vertical;
            outline: none;
            transition: border-color 0.2s, box-shadow 0.2s;
            background: #fafafa;
        }

        textarea:focus {
            border-color: #6366f1;
            background: #fff;
            box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
        }

        .btn-group {
            display: flex;
            gap: 12px;
            margin-top: 14px;
        }

        .btn-generate {
            flex: 1;
            padding: 14px;
            background: linear-gradient(135deg, #6366f1, #8b5cf6);
            color: white;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
        }

        .btn-generate:hover:not(:disabled) {
            transform: translateY(-1px);
            box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
        }

        .btn-generate:disabled {
            opacity: 0.6;
            cursor: not-allowed;
        }

        .btn-clear {
            padding: 14px 24px;
            background: #f3f4f6;
            color: #4b5563;
            border: 1px solid #e5e7eb;
            border-radius: 12px;
            font-size: 15px;
            cursor: pointer;
            transition: all 0.2s;
        }

        .btn-clear:hover {
            background: #e5e7eb;
        }

        /* 结果区域 */
        .result-section {
            margin-top: 32px;
            position: relative;
            min-height: 200px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        .loading-box {
            display: none;
            flex-direction: column;
            align-items: center;
            gap: 16px;
            color: #6b7280;
        }

        .spinner {
            width: 40px;
            height: 40px;
            border: 3px solid #e5e7eb;
            border-top-color: #6366f1;
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .error-tip {
            color: #dc2626;
            background: #fef2f2;
            border: 1px solid #fecaca;
            padding: 12px 16px;
            border-radius: 8px;
            width: 100%;
            text-align: center;
            display: none;
            font-size: 14px;
        }

        .result-wrapper {
            width: 100%;
            display: none;
            flex-direction: column;
            align-items: center;
            gap: 16px;
        }

        .result-img {
            max-width: 100%;
            max-height: 520px;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
        }

        .btn-download {
            padding: 10px 28px;
            background: #10b981;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            cursor: pointer;
            transition: background 0.2s;
        }

        .btn-download:hover {
            background: #059669;
        }

        /* 响应式适配 */
        @media (max-width: 640px) {
            .container {
                padding: 24px 20px;
            }
            h1 {
                font-size: 22px;
            }
            .btn-group {
                flex-direction: column;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Flux 写实图像生成器</h1>
        <p class="subtitle">基于 Hugging Face Flux-RealismLora 模型 · 一键生成写实风格图像</p>

        <!-- 示例提示词 -->
        <div class="prompt-examples">
            <label>快捷提示词(点击填充)</label>
            <div class="example-tags">
                <span class="example-tag" data-prompt="雨后的城市步行街,傍晚黄金时刻,路面有积水倒影,行人撑伞走过,街边店铺暖光,长焦镜头拍摄,颗粒感,真实街拍,高细节">城市街拍</span>
                <span class="example-tag" data-prompt="秋日森林中的木屋,清晨薄雾,阳光透过树叶洒下,木质纹理清晰,8K写实摄影,自然光影">森林木屋</span>
                <span class="example-tag" data-prompt="窗边的咖啡杯,午后阳光,木质桌面,景深虚化,胶片质感,暖色调,静物写实摄影">静物咖啡</span>
                <span class="example-tag" data-prompt="海边日落,沙滩上的脚印,海浪拍岸,橙紫色天空,广角镜头,真实旅拍质感">海边日落</span>
            </div>
        </div>

        <div class="input-section">
            <textarea id="promptInput" placeholder="请输入图像描述,越详细写实效果越好..."></textarea>
            <div class="btn-group">
                <button class="btn-generate" id="generateBtn">生成图像</button>
                <button class="btn-clear" id="clearBtn">清空</button>
            </div>
        </div>

        <div class="result-section">
            <div class="loading-box" id="loadingBox">
                <div class="spinner"></div>
                <span>正在生成图像,请稍候...</span>
            </div>
            <div class="error-tip" id="errorTip"></div>
            <div class="result-wrapper" id="resultWrapper">
                <img class="result-img" id="resultImg" alt="生成结果">
                <button class="btn-download" id="downloadBtn">下载图片</button>
            </div>
        </div>
    </div>

    <script>
        const generateBtn = document.getElementById("generateBtn");
        const clearBtn = document.getElementById("clearBtn");
        const promptInput = document.getElementById("promptInput");
        const loadingBox = document.getElementById("loadingBox");
        const errorTip = document.getElementById("errorTip");
        const resultWrapper = document.getElementById("resultWrapper");
        const resultImg = document.getElementById("resultImg");
        const downloadBtn = document.getElementById("downloadBtn");
        const exampleTags = document.querySelectorAll(".example-tag");

        // 示例提示词点击填充
        exampleTags.forEach(tag => {
            tag.addEventListener("click", () => {
                promptInput.value = tag.dataset.prompt;
            });
        });

        // 清空按钮
        clearBtn.addEventListener("click", () => {
            promptInput.value = "";
            resultWrapper.style.display = "none";
            errorTip.style.display = "none";
        });

        // 生成按钮
        generateBtn.addEventListener("click", async () => {
            const prompt = promptInput.value.trim();
            if (!prompt) {
                showError("请输入图像描述");
                return;
            }

            // 重置状态
            errorTip.style.display = "none";
            resultWrapper.style.display = "none";
            loadingBox.style.display = "flex";
            generateBtn.disabled = true;
            generateBtn.textContent = "生成中...";

            try {
                const res = await fetch("/api/generate", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({ prompt: prompt })
                });

                const data = await res.json();
                if (data.success) {
                    resultImg.src = data.image;
                    resultWrapper.style.display = "flex";
                } else {
                    showError(data.error || "生成失败");
                }
            } catch (err) {
                showError("网络错误,请检查后端服务是否正常运行");
            } finally {
                loadingBox.style.display = "none";
                generateBtn.disabled = false;
                generateBtn.textContent = "生成图像";
            }
        });

        // 下载图片
        downloadBtn.addEventListener("click", () => {
            const link = document.createElement("a");
            link.href = resultImg.src;
            link.download = `flux_生成图_${Date.now()}.png`;
            link.click();
        });

        function showError(msg) {
            errorTip.textContent = msg;
            errorTip.style.display = "block";
        }
    </script>
</body>
</html>

(5)调用成功和前端界面

678E0FDE7E15F8EEE8DEAA52DE2936B8

1A8C39B16BF9978DB772252FEDDA02B9

2.2 提示词设计思路和修改过程

(1)第一次

提示词:海边日落,沙滩上的脚印,海浪拍岸,橙紫色天空,广角镜头,真实旅拍质感
生成图片:

flux_生成图_1789034639261
第一次生成的图片整体写实风格不错,但和提示词有几处明显偏差:明明写了 "日落" 和 "橙紫色天空",结果生成的是大白天的蓝天海景,完全没体现日落时分的暖色调;沙滩上的脚印也没出来。主要原因是提示词用中文写,Flux 模型对英文理解更精准,而且 "日落"" 旅拍质感 " 这类词太笼统,模型不知道具体怎么表现。

(2)第二次

提示词:Golden hour sunset at a tropical beach, golden and purple sky, soft orange glow on the horizon, waves gently crashing on the shore, footprints in the sand in the foreground, wide angle lens, shot on Sony A7IV, 24mm f/2.8, warm color grading, film grain, travel photography style, photorealistic, 8k detail
生成图片:

flux_生成图_1789034882997
这次效果非常好!升级成英文提示词之后,所有关键元素都准确呈现了:日落时分的太阳挂在海平面上,天空呈现出温暖的橙色调,云彩也被染成金色,前景沙滩上有清晰的一串脚印,海浪轻轻拍打着岸边,整体暖金色调非常有旅拍氛围感。对比第一次的白天蓝天,这次的改进非常明显 —— 说明用英文提示词并加上具体的摄影参数和光线描述,能让 Flux 模型更精准地理解你的意图,生成更贴近真实世界的图像。

2.3 体验与心得

这次实践让我体会最深的是提示词设计对生成效果的决定性影响。第一次用中文写提示词,虽然描述了想要的元素,但模型很多细节都没准确理解,出来的效果和预期差很远。后来改成英文并加上具体的光线、摄影参数描述,效果立刻精准了很多。这说明和 AI 模型沟通时,语言和细节都很重要——用模型熟悉的语言,把抽象的感受转化为具体的视觉参数,才能得到想要的结果。另外,从 API 调用到前后端联调的整个流程跑下来,也让我对 AI 服务的实际落地方式有了更直观的理解。

三、GitHub个人主页

80CFCBC644CF3B9B2BE9051CCD8B891C

四、博客园随笔

博客园编辑

5F27B5B393008FA7D52053325630E271

4.1已具备的专业知识与能力

  • 能力 A:C/C++、Python 编程能力
    能够独立完成基础程序编写,掌握基础语法、循环、函数、结构体等内容;可以独立实现简单算法、小型实验代码,能够看懂并调试基础代码。
    • 能力 B:AI 辅助搭建小型项目
      会借助 AI 工具完成需求拆解、代码生成、bug 排查,能够从零搭建简易的小型 Demo 项目,具备基础项目落地能力。
  • 前后端基础知识了解
    对前端基础页面、后端服务基本概念有认知,能看懂简单前后端代码,理解基础请求交互逻辑,可完成简单页面与接口调试。
  • 了解开源社区
    知道开源项目基本概念,能够在 Github 浏览项目、查阅文档,掌握基础仓库查看、克隆操作。

4.2感兴趣的技术方向

硬件开发、嵌入式底层开发、大型工程项目开发、开源项目协同开发。

4.3欠缺的能力

  • 项目底层原理理解薄弱,阅读大型工程源码困难,对底层执行逻辑不熟悉;
  • 工程化开发能力不足:缺少规范的项目架构设计、模块拆分、单元测试经验;
  • 多人协同开发经验匮乏,不熟悉 Git 多人分支管理、代码评审、项目任务分工等团队开发流程;
  • 硬件与软件联合调试经验不足,软硬件协同开发的实战经验较少;
  • 代码健壮性不足,缺少异常处理、性能优化的实战经验。

4.4代码量说明

  • 当前累计有效代码量:3000~4000 行
  • 本学期课程结束目标代码量:6000 行

4.5课程期待

最期待学习多人团队协作开发大型项目相关知识,掌握工程化项目搭建、版本协同管理、项目模块划分、团队任务管理的完整流程。希望在课程中锻炼大型项目开发能力,学会规范开发流程,提升阅读复杂项目底层代码的能力,积累团队项目实战经验;同时希望接触软硬件结合项目的开发思路,补齐软硬件协同开发的短板。

4.6软件工程课程学习指南

模块 内容
课程目标 掌握工业化软件项目开发全流程,规范完成需求分析、方案设计、编码实现、测试与项目交付;掌握团队协同开发方法,建立工程化思维,跳出零散代码编写。
学习路线 1. 基础认知阶段:理解软件生命周期、版本管理等概念,掌握 Git 基础操作2. 个人小型工程实践:学习模块拆分、编码规范、单元测试,完成小型 Demo3. 团队项目实战:多人协作开发,任务拆解、接口约定、代码评审,完成完整项目4. 复盘优化阶段:项目复盘,学习阅读开源项目,加深底层逻辑理解
推荐工具栈 版本控制:Git + GitHub/Gitee代码编辑器:VS Code文档:Markdown 笔记任务管理:飞书文档 / TrelloAI 辅助工具:DeepSeek,用于知识点答疑、文档撰写、代码排错
学习重点 1. 需求分析:将模糊需求转化为清晰功能规格,划分模块2. 软件设计:模块解耦、接口设计、分层架构思想3. 编码规范:统一代码风格,完善注释,提升健壮性4. 软件测试:单元测试、基础功能测试5. 团队协作:Git 分支协同、任务分工、代码评审
本学期学习计划 第 1–4 周:软件工程基础理论,熟练 Git,完成个人技能自评第 5–10 周:个人工程练习,持续编码,阅读开源源码第 11–16 周:团队大项目开发,完成设计、编码、联调、测试期末:项目答辩、项目复盘,整理学习文档
难点与对策 难点 1:大型项目底层逻辑难懂 → 从小开源项目分段读源码,AI 辅助理解执行流程难点 2:多人 Git 协作冲突 → 遵守分支规范,频繁提交、及时合并、代码评审难点 3:软硬件联合开发调试 → 提前定义软硬件接口,分步调试,区分软硬故障
AI 工具使用原则 使用 DeepSeek 作为辅助,禁止直接复制成品代码。可用于概念理解、需求梳理、文档撰写、bug 定位、方案启发;核心逻辑、主体代码、项目思考自主完成。
预期收获 1. 掌握软件工程完整开发流程,建立工程化思维2. 熟练 Git 多人协同,积累团队项目实战经验3. 提升大型源码阅读能力,补齐底层原理短板4. 积累工程代码,达成 6000 行代码目标5. 掌握软硬件结合项目开发思路,铺垫硬件、嵌入式方向学习
posted @ 2026-09-10 20:06  astronomyy  阅读(2)  评论(0)    收藏  举报