File size: 7,086 Bytes
6efc50c
 
 
 
272b97f
6efc50c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0efc7f3
6efc50c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import os
import torch
import gradio as gr
from transformers import AutoTokenizer, AutoModelForCausalLM

# ========= 基本設定 =========
# 你的模型 repo id(目前是 private 也沒關係)
MODEL_ID = "aciang/mistral7b-tk-sft-20251019-merged"

# 若模型是 private,建議在 Space 的「Settings → Repository secrets」加上 HF_TOKEN
HF_TOKEN = os.getenv("HF_TOKEN", None)

# 建議預設的「傳統知識」系統提示,可以在介面中修改
DEFAULT_SYSTEM_PROMPT = """你是一位熟悉台灣與國際原住民族傳統知識的學者,
擅長用淺顯但尊重文化脈絡的繁體中文說明各族的夢境、儀式、宇宙觀、傳統醫療與環境知識。

回答原則:
1. 先簡短摘要重點(3–5 點條列)。
2. 儘量說明「族名、場域、情境」與「知識來源背景」,避免抽象空話。
3. 若是推論或類比,要清楚標註「推測」而不是說成唯一正解。
4. 若資料不足或超出目前教材範圍,請誠實說明,並給出安全的延伸建議。
5. 全程使用繁體中文。"""

# ========= 載入模型 =========

print(f"載入模型:{MODEL_ID} ...")

tokenizer = AutoTokenizer.from_pretrained(
    MODEL_ID,
    use_auth_token=HF_TOKEN,
)

# 保險起見,若沒有 pad_token 就沿用 eos_token
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.float16,
    device_map="auto",          # 自動分配到 GPU
    use_auth_token=HF_TOKEN,
)

model.eval()
print("模型載入完成。")


# ========= 建立提示詞 =========

def build_prompt(system_prompt: str, history: list[tuple[str, str]], user_message: str) -> str:
    """
    將 system_prompt + 歷史對話 + 新問題 組成一段文字 prompt。
    這裡用簡單的「使用者 / 助手」格式,對傳統知識生成已經很足夠。
    """
    system_prompt = system_prompt.strip()
    prompt = f"[系統提示]\n{system_prompt}\n\n"

    # 過去對話(若有)
    if history:
        prompt += "[對話紀錄]\n"
        for i, (user, bot) in enumerate(history, start=1):
            prompt += f"輪次 {i}:\n使用者:{user}\n助手:{bot}\n\n"

    # 最新一輪問題
    prompt += "[目前問題]\n"
    prompt += f"使用者:{user_message}\n助手:"

    return prompt


# ========= 生成函式 =========

def generate_reply(
    user_message: str,
    chat_history: list[tuple[str, str]],
    system_prompt: str,
    temperature: float,
    max_new_tokens: int,
):
    if not user_message.strip():
        return chat_history, gr.update(value="")

    # 組合成一個大 prompt
    prompt_text = build_prompt(system_prompt, chat_history, user_message)

    inputs = tokenizer(
        prompt_text,
        return_tensors="pt",
        add_special_tokens=True,
    ).to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=int(max_new_tokens),
            do_sample=True,
            temperature=float(temperature),
            top_p=0.9,
            pad_token_id=tokenizer.eos_token_id,
        )

    # 只取新生成的部分
    input_len = inputs["input_ids"].shape[-1]
    generated_tokens = outputs[0, input_len:]
    answer = tokenizer.decode(generated_tokens, skip_special_tokens=True).strip()

    chat_history = chat_history + [(user_message, answer)]
    return chat_history, ""  # 清空輸入框


def clear_history():
    return [], ""


# ========= Gradio 介面 =========

with gr.Blocks(title="語言橋傳統知識聊天機器人 — Mistral7B TK", theme=gr.themes.Soft()) as demo:
    gr.Markdown(
        """
        # 語言橋傳統知識聊天機器人 — Mistral7B TK
        使用你自訓練的 **mistral7b-tk-sft-20251019-merged** 模型,離線在 Hugging Face Space 上回答與傳統知識相關的問題。

        建議題材舉例:
        - 不同族群對「夢境」的五種層次與詮釋差異  
        - 布農族狩獵儀式與祖靈信仰  
        - 排灣族階級制度與紋面、圖騰的意義  
        - 阿美族年齡階層制與植物分類知識  
        - 海外原住民(如 Inuit)對身體、疾病與療癒的理解  
        """
    )

    with gr.Row():
        # 左側:設定區
        with gr.Column(scale=1):
            system_prompt_box = gr.Textbox(
                label="系統提示(模型角色與回答風格)",
                value=DEFAULT_SYSTEM_PROMPT,
                lines=16,
            )
            temperature_slider = gr.Slider(
                label="溫度(創造性)",
                minimum=0.1,
                maximum=1.5,
                value=0.7,
                step=0.05,
            )
            max_tokens_slider = gr.Slider(
                label="最大回覆長度(token 數,大約字數的 1.5–2 倍)",
                minimum=64,
                maximum=1024,
                value=512,
                step=16,
            )
            gr.Markdown(
                """
                **小提醒:**
                - 回答太發散 → 降低溫度(0.4–0.7)。  
                - 回答太短 → 拉高「最大回覆長度」。  
                - Space 若常 timeout,可以稍微降低最大回覆長度。
                """
            )

        # 右側:聊天區
        with gr.Column(scale=2):
            chatbot = gr.Chatbot(
                label="傳統知識 Chatbot",
                height=480,
                show_copy_button=True,
            )
            user_input = gr.Textbox(
                label="輸入你的問題(可多輪對話)",
                placeholder="例如:請比較布農族、排灣族和阿美族對治療疾病與夢境預兆的不同理解方式。",
                lines=4,
            )
            with gr.Row():
                send_btn = gr.Button("送出問題", variant="primary")
                clear_btn = gr.Button("清除對話")

    # 狀態:對話歷史
    state = gr.State([])  # list[tuple[user, bot]]

    # 綁定互動
    send_btn.click(
        fn=generate_reply,
        inputs=[
            user_input,
            state,
            system_prompt_box,
            temperature_slider,
            max_tokens_slider,
        ],
        outputs=[chatbot, user_input],
    ).then(
        fn=lambda h: h,
        inputs=[chatbot],
        outputs=[state],
    )

    user_input.submit(
        fn=generate_reply,
        inputs=[
            user_input,
            state,
            system_prompt_box,
            temperature_slider,
            max_tokens_slider,
        ],
        outputs=[chatbot, user_input],
    ).then(
        fn=lambda h: h,
        inputs=[chatbot],
        outputs=[state],
    )

    clear_btn.click(
        fn=clear_history,
        inputs=[],
        outputs=[chatbot, user_input],
    ).then(
        fn=lambda: [],
        inputs=[],
        outputs=[state],
    )

# 在 HF Space 中不需要 demo.launch(),平台會自動呼叫 demo