File size: 5,116 Bytes
8987e34
79cceb8
8987e34
65038dc
8987e34
 
 
61119fe
65038dc
61119fe
8987e34
 
 
 
 
abaf7bb
8987e34
 
d3eab6a
8987e34
 
d3eab6a
79cceb8
 
8987e34
79cceb8
d3eab6a
8987e34
d3eab6a
65038dc
 
 
 
 
 
8987e34
 
d3eab6a
8987e34
 
d3eab6a
8987e34
 
79cceb8
8987e34
 
79cceb8
8987e34
 
d3eab6a
8987e34
d3eab6a
 
8987e34
d3eab6a
 
 
8987e34
d3eab6a
8987e34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
abaf7bb
8987e34
 
d3eab6a
8987e34
d3eab6a
8987e34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79cceb8
8987e34
 
 
 
 
 
 
 
79cceb8
8987e34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61119fe
 
8987e34
 
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
# app.py
import os
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"))
# ===================================


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 -> 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 _post_chat(messages: List[Dict[str, Any]], temperature: float = 0.7, max_tokens: Optional[int] = None) -> str:
    """
    直接请求 StepFun 的 /v1/chat/completions(OpenAI 兼容)。
    返回纯字符串,避免 Gradio schema 问题。
    """
    api_key = _get_api_key()
    if not api_key:
        raise RuntimeError(
            "未检测到 API Key。请到 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,
        # StepFun 多数情况下无需强制 max_tokens;需要时再放开
    }
    if max_tokens is not None:
        payload["max_tokens"] = max_tokens

    with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
        resp = client.post(url, headers=headers, json=payload)
        # 让 httpx 抛出更清晰的错误
        resp.raise_for_status()
        data = resp.json()

    # 标准 OpenAI 兼容返回
    try:
        return str(data["choices"][0]["message"]["content"])
    except Exception:
        # 返回原始数据便于诊断
        return f"[WARN] 无法解析返回格式:{data}"


def chat_with_step3(image: Optional[Image.Image], question: str, temperature: float) -> str:
    """
    Gradio 的回调函数:接收 PIL 图片与文本,返回字符串。
    """
    if image is None and not question.strip():
        return "请上传一张图片,或至少输入一个问题。"

    # 构造 messages(支持纯文本、纯图像,或图文混合)
    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}]

    try:
        return _post_chat(messages, temperature=temperature)
    except httpx.HTTPStatusError as e:
        # 返回服务端 HTTP 错误 + 文本体,便于排查
        try:
            detail = e.response.text
        except Exception:
            detail = repr(e)
        return f"[HTTP {e.response.status_code}] 接口错误:{detail}"
    except Exception as e:
        return f"调用失败:{e!r}"


# ================ Gradio UI ================
with gr.Blocks(title="Step3 (StepFun API Demo)") 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=8)

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

    gr.Markdown(
        """
        **提示:**
        - 如果看到 `调用失败:RuntimeError('未检测到 API Key')`,请检查 Space 的 Secrets
        - 如需改模型:设置环境变量 `STEPFUN_MODEL`,或在代码顶部修改默认值
        """
    )

if __name__ == "__main__":
    # HF Space 环境会自动执行;本地运行也 OK
    demo.launch()