File size: 6,205 Bytes
8987e34
79cceb8
da75606
 
 
8987e34
65038dc
8987e34
 
 
61119fe
65038dc
61119fe
aeb0af0
8987e34
aeb0af0
8987e34
aeb0af0
 
abaf7bb
8987e34
 
d3eab6a
aeb0af0
 
d3eab6a
79cceb8
 
8987e34
79cceb8
d3eab6a
aeb0af0
d3eab6a
65038dc
 
 
 
 
 
8987e34
aeb0af0
 
 
 
 
 
 
 
 
 
 
 
 
d3eab6a
8987e34
aeb0af0
d3eab6a
8987e34
 
aeb0af0
 
 
 
8987e34
 
d3eab6a
8987e34
d3eab6a
 
8987e34
d3eab6a
 
 
 
8987e34
 
 
 
aeb0af0
 
 
 
 
 
 
 
 
 
 
 
 
 
abaf7bb
8987e34
 
d3eab6a
aeb0af0
 
d3eab6a
aeb0af0
 
 
 
8987e34
aeb0af0
 
 
 
8987e34
aeb0af0
 
 
 
8987e34
aeb0af0
8987e34
79cceb8
aeb0af0
 
8987e34
 
aeb0af0
 
8987e34
 
 
 
aeb0af0
8987e34
 
 
 
 
aeb0af0
8987e34
 
 
aeb0af0
8987e34
 
 
 
 
aeb0af0
 
 
 
8987e34
 
61119fe
 
da75606
f9d6da9
da75606
f9d6da9
 
da75606
f9d6da9
da75606
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# app.py
import os
# 关闭 /info 路由,绕开 gradio_client 的 schema 生成坑
os.environ["GRADIO_SHOW_API"] = "false"

import io
import base64
from typing import List, Dict, Any, Optional

import httpx
import gradio as gr
from PIL import Image

# ========= 基本配置(可用环境变量覆写)=========
STEPFUN_ENDPOINT = os.getenv("STEPFUN_ENDPOINT", "https://api.stepfun.com/v1")
MODEL_NAME = os.getenv("STEPFUN_MODEL", "step-3")  # 可改为 step-r1-v-mini
REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "60"))
MAX_CHARS = int(os.getenv("MAX_CHARS", "20000"))   # 返回文本最大展示长度
# ===========================================


def _get_api_key() -> Optional[str]:
    """
    优先读 OPENAI_API_KEY(OpenAI 兼容习惯),否则读 STEPFUN_KEY。
    在 HF Space → Settings → Variables and secrets 添加其中一个即可。
    """
    return os.getenv("OPENAI_API_KEY") or os.getenv("STEPFUN_KEY")


def _pil_to_data_url(img: Image.Image, fmt: str = "PNG") -> str:
    """
    PIL.Image -> data:image/...;base64,... 字符串(适配 OpenAI 兼容的 image_url)
    """
    buf = io.BytesIO()
    img.save(buf, format=fmt)
    b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
    mime = "image/png" if fmt.upper() == "PNG" else "image/jpeg"
    return f"data:{mime};base64,{b64}"


def _truncate(text: str, limit: int = MAX_CHARS) -> str:
    """
    软截断,避免一次性把超长内容写给前端导致传输异常。
    """
    if text is None:
        return ""
    if len(text) <= limit:
        return text
    return text[:limit] + "\n\n[输出过长,已截断]"


def _post_chat(messages: List[Dict[str, Any]], temperature: float = 0.7,
               max_tokens: Optional[int] = None) -> str:
    """
    直接请求 StepFun 的 /v1/chat/completions(OpenAI 兼容)。
    返回纯字符串,不抛异常,交由上层统一处理。
    """
    api_key = _get_api_key()
    if not api_key:
        # 不 raise,让 UI 只显示字符串,避免 Uvicorn/h11 生成异常页
        return ("[配置错误] 未检测到 API Key。\n"
                "请到 Space 的 Settings → Variables and secrets 添加:\n"
                "  OPENAI_API_KEY=你的 StepFun API Key   (或使用 STEPFUN_KEY)")

    url = f"{STEPFUN_ENDPOINT.rstrip('/')}/chat/completions"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    payload: Dict[str, Any] = {
        "model": MODEL_NAME,
        "messages": messages,
        "temperature": temperature,
    }
    if max_tokens is not None:
        payload["max_tokens"] = max_tokens

    try:
        with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
            resp = client.post(url, headers=headers, json=payload)
            resp.raise_for_status()
            data = resp.json()
        # 标准 OpenAI 兼容返回
        content = data["choices"][0]["message"]["content"]
        return _truncate(str(content))
    except httpx.HTTPStatusError as e:
        body = e.response.text if e.response is not None else repr(e)
        code = getattr(e.response, "status_code", "?")
        return _truncate(f"[HTTP {code}] 接口错误:\n{body}")
    except Exception as e:
        # 网络/解析/其他错误
        return _truncate(f"[调用失败] {repr(e)}")


def chat_with_step3(image: Optional[Image.Image], question: str, temperature: float) -> str:
    """
    Gradio 回调:接收图片与问题文本,返回字符串。
    任何异常都“吃掉”,只返回文本,防止框架层渲染异常页。
    """
    try:
        # 输入兜底
        if image is None and not question.strip():
            return "请上传一张图片,或至少输入一个问题。"

        content: List[Dict[str, Any]] = []
        if image is not None:
            data_url = _pil_to_data_url(image, fmt="PNG")
            content.append({"type": "image_url", "image_url": {"url": data_url}})

        if question.strip():
            content.append({"type": "text", "text": question.strip()})
        else:
            content.append({"type": "text", "text": "请描述这张图片。"})

        messages = [{"role": "user", "content": content}]
        return _post_chat(messages, temperature=temperature)
    except Exception as e:
        # 再兜一层底,避免任何未捕获异常冒泡
        return _truncate(f"[运行时错误] {repr(e)}")


# ================== Gradio UI ==================
with gr.Blocks(title="Step3 (StepFun API Demo)", analytics_enabled=False) as demo:
    gr.Markdown(
        """
        # Step3 · 图文对话演示(StepFun OpenAI 兼容接口)
        - 在 **Settings → Variables and secrets** 添加 `OPENAI_API_KEY`(或 `STEPFUN_KEY`)后即可使用  
        - 后端通过 `https://api.stepfun.com/v1/chat/completions`,不依赖 `openai` SDK  
        """
    )

    with gr.Row():
        image = gr.Image(type="pil", label="上传图片(可选)")

    question = gr.Textbox(label="问题", placeholder="例如:帮我看看这是什么菜,怎么做?")
    temperature = gr.Slider(0.0, 1.5, value=0.7, step=0.1, label="Temperature")
    submit = gr.Button("提交", variant="primary")
    output = gr.Textbox(label="模型回答", lines=12)

    submit.click(fn=chat_with_step3, inputs=[image, question, temperature], outputs=[output])

    gr.Markdown(
        """
        **小贴士:**
        - 如见到 `[配置错误] 未检测到 API Key`,请检查 Space 的 Secrets  
        - 如需改模型:设置环境变量 `STEPFUN_MODEL`,或在代码顶部修改默认值  
        - 如输出非常长,会自动做软截断避免传输异常  
        """
    )

if __name__ == "__main__":
    # 兼容不同 gradio 版本的 queue() 参数签名;同时关闭 API/错误页渲染
    try:
        demo.queue(concurrency_count=2, max_size=32).launch(show_api=False, show_error=False, quiet=True)
    except TypeError:
        try:
            demo.queue(2).launch(show_api=False, show_error=False, quiet=True)
        except TypeError:
            demo.queue().launch(show_api=False, show_error=False, quiet=True)