Spaces:
Running
Running
import gradio as gr | |
import pandas as pd | |
import plotly.graph_objects as go | |
import hashlib, tempfile, os, time | |
from datetime import datetime, timezone | |
import sqlite3 | |
import random | |
# 假设这些模块在其他地方定义 | |
from config import CSS, DIMS | |
from OVAL import oval_scores | |
from DeepEval import deepeval_scores | |
# 全局配置 | |
DAILY_LIMIT = 150 # 每日全局限制次数 | |
REQUEST_INTERVAL = 9 # 请求间隔(秒) | |
DB_FILE = "usage_tracker.db" # SQLite数据库文件名 | |
def init_db(): | |
"""初始化SQLite数据库""" | |
conn = sqlite3.connect(DB_FILE) | |
c = conn.cursor() | |
# 创建全局计数器表 | |
c.execute(''' | |
CREATE TABLE IF NOT EXISTS global_stats ( | |
id INTEGER PRIMARY KEY, | |
date TEXT NOT NULL, | |
count INTEGER NOT NULL, | |
last_request REAL NOT NULL | |
) | |
''') | |
# 确保只有一条记录 | |
c.execute("SELECT COUNT(*) FROM global_stats") | |
count = c.fetchone()[0] | |
if count == 0: | |
c.execute("INSERT INTO global_stats (date, count, last_request) VALUES (?, ?, ?)", | |
(get_utc_date(), 0, time.time())) | |
conn.commit() | |
conn.close() | |
def get_utc_date(): | |
"""获取UTC+0的日期字符串""" | |
return datetime.now(timezone.utc).strftime("%Y-%m-%d") | |
def check_daily_limit(): | |
"""检查今日全局请求次数是否超限""" | |
today = get_utc_date() | |
conn = sqlite3.connect(DB_FILE) | |
c = conn.cursor() | |
c.execute("SELECT date, count, last_request FROM global_stats WHERE id = 1") | |
row = c.fetchone() | |
if not row: | |
# 如果记录不存在,初始化 | |
c.execute("INSERT INTO global_stats (date, count, last_request) VALUES (?, ?, ?)", | |
(today, 0, time.time())) | |
count = 0 | |
else: | |
db_date, count, last_request = row | |
# 如果是新的一天,重置计数 | |
if db_date != today: | |
c.execute("UPDATE global_stats SET date = ?, count = ?, last_request = ? WHERE id = 1", | |
(today, 0, time.time())) | |
count = 0 | |
conn.commit() | |
conn.close() | |
return count >= DAILY_LIMIT, count | |
def update_request_count(): | |
"""更新全局请求计数""" | |
today = get_utc_date() | |
current_time = time.time() | |
conn = sqlite3.connect(DB_FILE) | |
c = conn.cursor() | |
c.execute("SELECT date, count FROM global_stats WHERE id = 1") | |
row = c.fetchone() | |
if not row: | |
# 如果记录不存在,初始化 | |
c.execute("INSERT INTO global_stats (date, count, last_request) VALUES (?, ?, ?)", | |
(today, 1, current_time)) | |
count = 1 | |
else: | |
db_date, count = row | |
# 如果是新的一天,重置计数 | |
if db_date != today: | |
c.execute("UPDATE global_stats SET date = ?, count = 1, last_request = ? WHERE id = 1", | |
(today, current_time)) | |
count = 1 | |
else: | |
# 增加计数 | |
c.execute("UPDATE global_stats SET count = count + 1, last_request = ? WHERE id = 1", | |
(current_time,)) | |
count += 1 | |
conn.commit() | |
conn.close() | |
return count, current_time | |
def check_request_interval(): | |
"""检查请求间隔是否满足要求""" | |
conn = sqlite3.connect(DB_FILE) | |
c = conn.cursor() | |
c.execute("SELECT last_request FROM global_stats WHERE id = 1") | |
row = c.fetchone() | |
if not row: | |
return True # 如果记录不存在,允许请求 | |
last_time = row[0] | |
conn.close() | |
return time.time() - last_time >= REQUEST_INTERVAL | |
def generate_captcha(): | |
"""生成随机加法验证码""" | |
num1 = random.randint(2, 8) | |
num2 = random.randint(2, 8) | |
return f"What's {num1} + {num2}?", num1 + num2 | |
def make_explanation(system: str, dimension: str, score: float) -> str: | |
templates = { | |
# OVAL 拓展 5 维 | |
"Structural Clarity": f"{system} scored Structural Clarity at {score}: The text structure may be unclear; consider adding headings or breaking into paragraphs.", | |
"Reasoning Quality": f"{system} scored Reasoning Quality at {score}: Argument support is weak; consider adding logical reasoning or evidence.", | |
"Factuality": f"{system} scored Factuality at {score}: Information may be inaccurate; please fact-check the facts.", | |
"Depth of Analysis": f"{system} scored Depth of Analysis at {score}: Analysis seems shallow; add more insights or examples.", | |
"Topic Coverage": f"{system} scored Topic Coverage at {score}: Key aspects may be missing; ensure you cover the full scope.", | |
# DeepEval 拓展 5 维 | |
"Fluency": f"{system} scored Fluency at {score}: Expression may be disfluent; consider smoothing sentence transitions.", | |
"Prompt Relevance": f"{system} scored Prompt Relevance at {score}: The response may stray from the prompt; ensure alignment.", | |
"Conciseness": f"{system} scored Conciseness at {score}: The response may be verbose; consider trimming redundant parts.", | |
"Readability": f"{system} scored Readability at {score}: The text is hard to read; consider simpler wording or shorter sentences.", | |
"Engagement": f"{system} scored Engagement at {score}: The response lacks engagement; add examples or a conversational tone.", | |
} | |
return templates.get(dimension, f"{system} scored {dimension} at {score}: Low score detected; please review this aspect.") | |
def evaluate( | |
prompt_text: str, | |
output_text: str, | |
# Prompt 主观 5 维度 | |
s1: float, s2: float, s3: float, s4: float, s5: float, | |
# Prompt 主观解释 | |
e1: str, e2: str, e3: str, e4: str, e5: str, | |
# Judge 模块 | |
judge_llm: str, | |
ja1: float, ja2: float, ja3: float, ja4: float, ja5: float, | |
judge_remark: str, | |
# 额外备注 | |
remark: str, | |
# 验证码 | |
captcha_answer: str, | |
correct_answer: int, | |
# 会话状态 | |
session_state: dict | |
): | |
# 1) 验证全局请求状态 | |
is_limited, current_count = check_daily_limit() | |
# 检查是否达到每日限制 | |
if is_limited: | |
return ( | |
gr.update(visible=True), # 显示限制提示 | |
gr.update(visible=False), # 隐藏结果区域 | |
None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, current_count, None, None | |
) | |
# 检查请求间隔 | |
if not check_request_interval(): | |
with sqlite3.connect(DB_FILE) as conn: | |
c = conn.cursor() | |
c.execute("SELECT last_request FROM global_stats WHERE id = 1") | |
last_time = c.fetchone()[0] | |
remaining_time = REQUEST_INTERVAL - (time.time() - last_time) | |
raise gr.Error(f"请等待 {remaining_time:.1f} 秒后再试") | |
# 检查验证码 | |
try: | |
if int(captcha_answer) != correct_answer: | |
raise gr.Error("Verification code error, please try again") | |
except (ValueError, TypeError): | |
raise gr.Error("Please enter the correct verification code") | |
# 2) 更新全局请求计数 | |
count, last_request = update_request_count() | |
# 3) 验证 Prompt 主观低分必须解释 | |
for score, exp, label in [ | |
(s1, e1, "Clarity"), | |
(s2, e2, "Scope Definition"), | |
(s3, e3, "Intent Alignment"), | |
(s4, e4, "Bias / Induction"), | |
(s5, e5, "Efficiency"), | |
]: | |
if score < 3 and not exp.strip(): | |
raise gr.Error(f"{label} score < 3: please provide an explanation.") | |
# 4) 构造三组分数 | |
subj = [s1, s2, s3, s4, s5] + [None]*10 | |
# 获取完整的OVAL和DeepEval分数 | |
full_oval = oval_scores(output_text) | |
full_deep = deepeval_scores(prompt_text, output_text) | |
# 灰化指定的维度(将对应分数设为None) | |
# OVAL的Factuality(索引7)和Topic Coverage(索引9) | |
full_oval[7] = None # Factuality | |
full_oval[9] = None # Topic Coverage | |
# DeepEval的Prompt Relevance(索引11)及Conciseness(索引12) | |
full_deep[11] = None # Prompt Relevance | |
full_deep[12] = None # Conciseness | |
# 使用处理后的分数 | |
oval = full_oval | |
deep = full_deep | |
# 5) 自动低分解释 | |
auto_expls = [] | |
for system, scores, idxs in [ | |
("OVAL", oval, range(5,10)), | |
("DeepEval", deep, range(10,15)) | |
]: | |
for i in idxs: | |
sc = scores[i] | |
if sc is not None and sc < 3: | |
auto_expls.append(make_explanation(system, DIMS[i], sc)) | |
auto_text = "\n".join(auto_expls) or "All automated scores ≥ 3; no issues detected." | |
# 6) 构建 DataFrame(包含 Judge 信息列) | |
full_df = pd.DataFrame({ | |
"Dimension": DIMS, | |
"Subjective (Prompt)": subj, | |
"OVAL (Output)": oval, | |
"DeepEval (Output)": deep, | |
"Judge LLM": [judge_llm] * len(DIMS), | |
"Sensory Accuracy": [ja1] * len(DIMS), | |
"Emotional Engagement": [ja2] * len(DIMS), | |
"Flow & Naturalness": [ja3] * len(DIMS), | |
"Imagery Completeness": [ja4] * len(DIMS), | |
"Simplicity & Accessibility": [ja5] * len(DIMS), | |
"Judge Remarks": [judge_remark] * len(DIMS), | |
"Notes (Slang/Tech Terms)": [remark] * len(DIMS), | |
}) | |
# 7) 提取子表 | |
subj_df = full_df.iloc[0:5][["Dimension","Subjective (Prompt)"]] | |
oval_df = full_df.iloc[5:10][["Dimension","OVAL (Output)"]] | |
deep_df = full_df.iloc[10:15][["Dimension","DeepEval (Output)"]] | |
# 8) 构造雷达图(取三类分数最大值) | |
max_scores = [ | |
max([v for v in vals if v is not None]) if any(v is not None for v in vals) else 0 | |
for vals in zip(subj, oval, deep) | |
] | |
closed_dims = DIMS + [DIMS[0]] | |
r = max_scores + [max_scores[0]] | |
fig = go.Figure(go.Scatterpolar(r=r, theta=closed_dims, fill='toself')) | |
fig.update_layout( | |
polar=dict(radialaxis=dict(visible=True, range=[0,5])), | |
showlegend=False, | |
title="Final (Max) Scores Radar" | |
) | |
# 更新页面底部的计数器显示 | |
return ( | |
gr.update(visible=False), # 隐藏限制提示 | |
gr.update(visible=True), # 显示结果区域 | |
subj_df, | |
oval_df, | |
deep_df, | |
fig, | |
None, # 不生成CSV文件 | |
remark, | |
e1, e2, e3, e4, e5, | |
auto_text, | |
judge_llm, | |
ja1, ja2, ja3, ja4, ja5, | |
judge_remark, | |
*generate_captcha(), # 生成新的验证码 | |
count, # 返回当前全局计数 | |
"expanded", # Judge区块默认展开 | |
gr.update(value=f"Today Counts: {count}/{DAILY_LIMIT}") # 更新底部计数器 | |
) | |
def toggle_explain(v): | |
return gr.update(visible=(v<3)) | |
def check_daily_limit_state(): | |
"""检查全局状态并更新UI显示""" | |
is_limited, current_count = check_daily_limit() | |
return ( | |
gr.update(visible=is_limited), # 限制提示 | |
gr.update(visible=not is_limited), # 启用提交按钮 | |
gr.update(visible=not is_limited), # 显示结果区域 | |
f"Today Counts: {current_count}/{DAILY_LIMIT}", # 更新计数器文本 | |
gr.update(value=f"Today Counts: {current_count}/{DAILY_LIMIT}") # 更新底部计数器 | |
) | |
def show_personal_version_notice(): | |
"""显示个人版本提示""" | |
raise gr.Error("Only for coming personal version.") | |
def toggle_judge_section(visible): | |
"""切换Judge部分的显示状态""" | |
return gr.update(visible=(visible == "expanded")), gr.update(value=("Collapse" if visible == "expanded" else "Expand")) | |
css = """ | |
#submit-btn { | |
background-color: orange !important; | |
color: white !important; | |
border: none !important; | |
} | |
#submit-btn:hover { | |
background-color: darkorange !important; | |
} | |
.limit-notice { | |
background-color: #ffcccc; | |
border: 1px solid #ff6666; | |
padding: 10px; | |
border-radius: 5px; | |
margin: 10px 0; | |
} | |
.upgrade-notice { | |
background-color: #e6f7ff; | |
border: 1px solid #91d5ff; | |
padding: 10px; | |
border-radius: 5px; | |
margin: 10px 0; | |
} | |
.welcome-notice { | |
background-color: #fff7e6; | |
border: 1px solid #ffd591; | |
padding: 10px; | |
border-radius: 5px; | |
margin: 10px 0; | |
} | |
.disabled-dimension { | |
color: #888; | |
font-style: italic; | |
} | |
.example-label { | |
font-weight: bold; | |
color: #666; | |
margin-top: 10px; | |
} | |
.daily-count { | |
font-size: 16px; | |
font-weight: bold; | |
margin-top: 15px; | |
text-align: center; | |
} | |
.judge-section { | |
border: 1px solid #ddd; | |
border-radius: 5px; | |
margin-top: 10px; | |
} | |
.judge-header { | |
cursor: pointer; | |
padding: 10px; | |
background-color: #f5f5f5; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
.judge-content { | |
padding: 10px; | |
} | |
""" | |
# 初始化数据库 | |
init_db() | |
with gr.Blocks(css=css) as iface: | |
# 会话状态 | |
session_state = gr.State({}) | |
judge_section_state = gr.State("expanded") # 初始为展开状态 | |
# 顶部欢迎语和限制说明 | |
gr.Markdown(""" | |
<div class="welcome-notice"> | |
<h3>👋 Hey there! You're using the ECHOscore demo.</h3> | |
<p>It's a lighter version with limited features.</p> | |
<p>For the full power, grab the desktop version(coming soon)!</p> | |
</div> | |
""") | |
# 每日限制提示(初始隐藏) | |
limit_notice = gr.Markdown(""" | |
<div class="limit-notice"> | |
<h3>⚠️ Oops! Daily limit reached.</h3> | |
<p>Tomorrow’s a new day — or skip the wait with desktop version (coming soon)!</p> | |
</div> | |
""", visible=False) | |
gr.Markdown("# ECHOscore – Prompt vs Output Evaluation") | |
# 当前使用情况 | |
daily_count = gr.Textbox(label="Daily Counts", value="Today Counts: 0/150", interactive=False, visible=False) | |
with gr.Row(): | |
prompt_in = gr.Textbox(lines=4, label="Input (Prompt)") | |
output_in = gr.Textbox(lines=4, label="Output (Model Response)") | |
# verification code | |
captcha_text = gr.Textbox(label="verification code", interactive=False) | |
captcha_answer = gr.Textbox(label="Please enter the calculation result", placeholder="Verification code answer") | |
correct_answer = gr.State(8) # 初始值,会在页面加载时更新 | |
with gr.Row(): | |
s1 = gr.Slider(0,5,0,step=0.1, label="Prompt – Clarity") | |
s2 = gr.Slider(0,5,0,step=0.1, label="Prompt – Scope Definition") | |
s3 = gr.Slider(0,5,0,step=0.1, label="Prompt – Intent Alignment") | |
s4 = gr.Slider(0,5,0,step=0.1, label="Prompt – Bias / Induction") | |
s5 = gr.Slider(0,5,0,step=0.1, label="Prompt – Efficiency") | |
e1 = gr.Textbox(lines=2, label="Explain Clarity (<3)", visible=False) | |
e2 = gr.Textbox(lines=2, label="Explain Scope Definition (<3)", visible=False) | |
e3 = gr.Textbox(lines=2, label="Explain Intent Alignment (<3)", visible=False) | |
e4 = gr.Textbox(lines=2, label="Explain Bias / Induction (<3)", visible=False) | |
e5 = gr.Textbox(lines=2, label="Explain Efficiency (<3)", visible=False) | |
remark = gr.Textbox(lines=2, label="Internet slang & technical terms notes (optional)") | |
# Judge模块 - 可折叠/展开 | |
with gr.Row(): | |
with gr.Column(scale=12): | |
judge_header = gr.Markdown(""" | |
<div class="judge-header"> | |
<span>LLM-as-a-Judge (optional)</span> | |
</div> | |
""") | |
with gr.Column(scale=1, visible=False): | |
toggle_judge_btn = gr.Button("Collapse", visible=False) | |
with gr.Row(visible=True) as judge_section: | |
judge_llm = gr.Textbox(lines=1, label="LLM-as-a-Judge (optional-Place the NAME of LLM)") | |
gr.Markdown("**LLM Scoring Examples**", elem_classes="example-label") | |
ja1 = gr.Number(label="Sensory Accuracy (only for desktop version)", value=0, precision=1, step=0.1, interactive=False) | |
ja2 = gr.Number(label="Emotional Engagement (only for desktop version)", value=0, precision=1, step=0.1, interactive=False) | |
ja3 = gr.Number(label="Flow & Naturalness (only for desktop version)", value=0, precision=1, step=0.1, interactive=False) | |
ja4 = gr.Number(label="Imagery Completeness (only for desktop version)", value=0, precision=1, step=0.1, interactive=False) | |
ja5 = gr.Number(label="Simplicity & Accessibility (only for desktop version)", value=0, precision=1, step=0.1, interactive=False) | |
judge_remark = gr.Textbox(lines=2, label="Judge Remarks (only for desktop version)", interactive=True) | |
# 升级提示 | |
gr.Markdown(""" | |
<div class="upgrade-notice"> | |
<h3>🔝 Unlock Full Features</h3> | |
<p>Get access to all dimensions and unlimited evaluations.</p> | |
<a href="https://www.echoscore.dev" target="_blank">Learn more about ECHOscore</a> | |
</div> | |
""") | |
s1.change(toggle_explain, s1, e1) | |
s2.change(toggle_explain, s2, e2) | |
s3.change(toggle_explain, s3, e3) | |
s4.change(toggle_explain, s4, e4) | |
s5.change(toggle_explain, s5, e5) | |
# 结果区域(初始隐藏) | |
with gr.Row(visible=False) as results_area: | |
subj_tbl = gr.Dataframe(label="Prompt Subjective Scores") | |
oval_tbl = gr.Dataframe(label="OVAL Automated Scores") | |
deep_tbl = gr.Dataframe(label="DeepEval Automated Scores") | |
radar = gr.Plot(label="Final Radar Chart") | |
csv_out = gr.File(label="Export CSV") | |
notes_out = gr.Textbox(label="Notes (Slang/Tech Terms)") | |
exp1_out = gr.Textbox(label="Clarity Explanation") | |
exp2_out = gr.Textbox(label="Scope Definition Explanation") | |
exp3_out = gr.Textbox(label="Intent Alignment Explanation") | |
exp4_out = gr.Textbox(label="Bias/Induction Explanation") | |
exp5_out = gr.Textbox(label="Efficiency Explanation") | |
auto_out = gr.Textbox(label="Automatic Explanation") | |
judge_llm_out = gr.Textbox(label="LLM-as-a-Judge") | |
ja1_out = gr.Number(label="Sensory Accuracy",visible=False) | |
ja2_out = gr.Number(label="Emotional Engagement",visible=False) | |
ja3_out = gr.Number(label="Flow & Naturalness",visible=False) | |
ja4_out = gr.Number(label="Imagery Completeness",visible=False) | |
ja5_out = gr.Number(label="Simplicity & Accessibility",visible=False) | |
judge_remarks_out = gr.Textbox(label="Judge Remarks") | |
submit = gr.Button("Submit", elem_id="submit-btn") | |
# 新增:创建一个用于显示底部计数器的组件 | |
footer_count = gr.Textbox(label="Today's Usage", value="Today Counts: 0/150", interactive=False, visible=True) | |
gr.Markdown(""" | |
<div> | |
⚠️ This is a **demo version** of ECHOscore. | |
Data contribution, uploads, and edits are **not supported**. | |
To try the full version, please download the desktop release. | |
</div> | |
""") | |
# 初始化检查 | |
iface.load( | |
check_daily_limit_state, | |
None, | |
[limit_notice, submit, results_area, daily_count, footer_count] # 添加footer_count | |
) | |
iface.load( | |
lambda: generate_captcha(), | |
None, | |
[captcha_text, correct_answer] | |
) | |
submit.click( | |
evaluate, | |
[ | |
prompt_in, output_in, | |
s1, s2, s3, s4, s5, | |
e1, e2, e3, e4, e5, | |
judge_llm, ja1, ja2, ja3, ja4, ja5, | |
judge_remark, remark, | |
captcha_answer, correct_answer, | |
session_state | |
], | |
[ | |
limit_notice, results_area, | |
subj_tbl, oval_tbl, deep_tbl, | |
radar, csv_out, notes_out, | |
exp1_out, exp2_out, exp3_out, exp4_out, exp5_out, | |
auto_out, | |
judge_llm_out, ja1_out, ja2_out, ja3_out, ja4_out, ja5_out, | |
judge_remarks_out, | |
captcha_text, correct_answer, | |
daily_count, | |
judge_section_state, # 更新Judge区块状态 | |
footer_count # 更新底部计数器 | |
] | |
) | |
# 点击CSV下载按钮时显示提示 | |
csv_out.download(show_personal_version_notice) | |
# 切换Judge部分的显示状态 | |
toggle_judge_btn.click( | |
lambda x: ("expanded" if x == "collapsed" else "collapsed"), | |
judge_section_state, | |
judge_section_state | |
).then( | |
toggle_judge_section, | |
judge_section_state, | |
[judge_section, toggle_judge_btn] | |
) | |
if __name__ == "__main__": | |
iface.launch() |