This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.215 強化 `/observability/ppt_audit_history` 視覺問題追蹤:將 `issues_found` 拆成投影片、問題類型、問題文字與回放入口,新增「視覺問題追蹤」面板,讓問題簡報能直接定位與預覽。
|
||||
- V10.213 補 `/observability/ppt_audit_history` 視覺 QA 診斷摘要:審核歷史與 Action Queue 直接顯示 `ppt_audit_results.issues_found` 的投影片問題,讓「有問題」可追查,不再只剩問題數或空白錯誤欄。
|
||||
- V10.212 修正 PPT 視覺 QA 的 Ollama 三主機 fallback:當 Primary/Secondary request timeout 超過 unhealthy TTL 時,第三輪仍強制打 111 final fallback;PPT 截圖送模型前轉輕量 JPEG、縮小輸出 token,降低單檔審核耗時。
|
||||
- V10.211 補 `/observability/ppt_audit_history` 全類型視覺 QA:審核歷史不再限 daily,頁面新增「立即視覺 QA」非阻塞補跑,結果寫入 `ppt_audit_results`;模型失敗時也保留 slide error,避免產線狀態只剩空白。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.214"
|
||||
SYSTEM_VERSION = "V10.215"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2818,6 +2818,48 @@ def ppt_audit_history():
|
||||
return ';'.join(snippets)
|
||||
return ';'.join(snippets)
|
||||
|
||||
def _load_ppt_issues(raw_issues):
|
||||
if not raw_issues:
|
||||
return []
|
||||
try:
|
||||
import json as _json
|
||||
issues_payload = _json.loads(raw_issues) if isinstance(raw_issues, str) else raw_issues
|
||||
except Exception:
|
||||
return []
|
||||
return issues_payload if isinstance(issues_payload, list) else []
|
||||
|
||||
def _classify_ppt_issue(issue_text: str):
|
||||
text = issue_text or ''
|
||||
if any(k in text for k in ['圖表', '切掉', '截斷', '超出', '溢出']):
|
||||
return '版面越界', 'error'
|
||||
if any(k in text for k in ['空白', '未填', '缺少', '無資料']):
|
||||
return '內容缺漏', 'warn'
|
||||
if any(k in text for k in ['低對比', '顏色', '字體', '字型', '閱讀']):
|
||||
return '可讀性', 'warn'
|
||||
return '視覺問題', 'warn'
|
||||
|
||||
def _extract_ppt_issue_items(raw_issues, *, pptx_filename: str, audited_at: str):
|
||||
issue_items = []
|
||||
for slide_item in _load_ppt_issues(raw_issues):
|
||||
if not isinstance(slide_item, dict):
|
||||
continue
|
||||
slide = slide_item.get('slide')
|
||||
slide_label = f"S{slide}" if slide else 'S?'
|
||||
for raw_issue in slide_item.get('issues') or []:
|
||||
issue_text = str(raw_issue).strip()
|
||||
if not issue_text:
|
||||
continue
|
||||
category, status = _classify_ppt_issue(issue_text)
|
||||
issue_items.append({
|
||||
'pptx_filename': pptx_filename,
|
||||
'audited_at': audited_at,
|
||||
'slide_label': slide_label,
|
||||
'category': category,
|
||||
'status': status,
|
||||
'text': issue_text,
|
||||
})
|
||||
return issue_items
|
||||
|
||||
# Phase 38+:讀指定月份 / 指定簡報類型 audit 歷史
|
||||
try:
|
||||
session = get_session()
|
||||
@@ -2836,24 +2878,45 @@ def ppt_audit_history():
|
||||
"""),
|
||||
audit_params,
|
||||
).fetchall()
|
||||
audit_records = [
|
||||
{
|
||||
'audited_at': r[0].strftime('%Y-%m-%d %H:%M'),
|
||||
'pptx_filename': r[1],
|
||||
audit_records = []
|
||||
for r in audit_rows:
|
||||
audited_at = r[0].strftime('%Y-%m-%d %H:%M')
|
||||
pptx_filename = r[1]
|
||||
raw_issues = r[7]
|
||||
audit_records.append({
|
||||
'audited_at': audited_at,
|
||||
'pptx_filename': pptx_filename,
|
||||
'audit_status': r[2],
|
||||
'issues_count': int(r[3] or 0),
|
||||
'confidence': float(r[4] or 0),
|
||||
'duration_ms': int(r[5] or 0),
|
||||
'error_msg': r[6],
|
||||
'issue_summary': _summarize_ppt_issues(r[7]),
|
||||
}
|
||||
for r in audit_rows
|
||||
]
|
||||
'issue_summary': _summarize_ppt_issues(raw_issues),
|
||||
'issue_items': _extract_ppt_issue_items(
|
||||
raw_issues,
|
||||
pptx_filename=pptx_filename,
|
||||
audited_at=audited_at,
|
||||
),
|
||||
})
|
||||
finally:
|
||||
session.close()
|
||||
except Exception:
|
||||
logger.debug("PPT audit history table unavailable; rendering empty audit history", exc_info=True)
|
||||
|
||||
issue_items = [
|
||||
issue
|
||||
for record in audit_records
|
||||
for issue in record.get('issue_items', [])
|
||||
]
|
||||
issue_files = {issue.get('pptx_filename') for issue in issue_items if issue.get('pptx_filename')}
|
||||
issue_digest = {
|
||||
'total': len(issue_items),
|
||||
'files': len(issue_files),
|
||||
'error_count': sum(1 for issue in issue_items if issue.get('status') == 'error'),
|
||||
'warn_count': sum(1 for issue in issue_items if issue.get('status') == 'warn'),
|
||||
'latest_audit': issue_items[0].get('audited_at') if issue_items else '',
|
||||
}
|
||||
|
||||
# PPT vision 啟用狀態
|
||||
vision_status = {'enabled': False, 'ready': False, 'blockers': ['視覺狀態讀取失敗']}
|
||||
try:
|
||||
@@ -3040,6 +3103,8 @@ def ppt_audit_history():
|
||||
generation_runs=generation_runs,
|
||||
pipeline_view=pipeline_view,
|
||||
vision_audit_filenames=vision_audit_filenames,
|
||||
issue_items=issue_items,
|
||||
issue_digest=issue_digest,
|
||||
error=error,
|
||||
)
|
||||
|
||||
|
||||
@@ -235,6 +235,49 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% if issue_items %}
|
||||
<section class="ppt-issue-board" aria-label="視覺問題追蹤">
|
||||
<div class="ppt-workbench-head">
|
||||
<div>
|
||||
<div class="ppt-label">Vision Findings</div>
|
||||
<h2 class="ppt-panel-title">視覺問題追蹤</h2>
|
||||
</div>
|
||||
<div class="ppt-issue-metrics">
|
||||
<span><strong>{{ issue_digest.total }}</strong> 問題</span>
|
||||
<span><strong>{{ issue_digest.files }}</strong> 檔案</span>
|
||||
<span><strong>{{ issue_digest.error_count }}</strong> 高優先</span>
|
||||
<span><strong>{{ issue_digest.latest_audit or '—' }}</strong> 最近審核</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ppt-issue-grid">
|
||||
{% for issue in issue_items[:8] %}
|
||||
<article class="ppt-issue-card is-{{ issue.status }}">
|
||||
<div class="ppt-issue-top">
|
||||
<span class="ppt-run-status is-{{ 'error' if issue.status == 'error' else 'planned' }}">{{ issue.slide_label }}</span>
|
||||
<span class="ppt-label">{{ issue.category }}</span>
|
||||
</div>
|
||||
<strong><code>{{ issue.pptx_filename }}</code></strong>
|
||||
<p>{{ issue.text }}</p>
|
||||
<div class="ppt-action-foot">
|
||||
<small class="text-muted">{{ issue.audited_at }}</small>
|
||||
<a class="btn btn-outline-primary btn-sm"
|
||||
href="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename) }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-ppt-open-preview
|
||||
data-ppt-filename="{{ issue.pptx_filename }}"
|
||||
data-ppt-preview-title="問題回放 · {{ issue.pptx_filename }}"
|
||||
data-ppt-preview-pdf="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename, action='pdf') }}"
|
||||
data-ppt-preview-page="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename) }}"
|
||||
data-ppt-download-url="{{ url_for('admin_observability.ppt_audit_file', filename=issue.pptx_filename, action='download') }}">
|
||||
<i class="fas fa-eye me-1"></i>回放
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
<section class="ppt-panel mt-3"
|
||||
data-ppt-auto-generation
|
||||
data-auto-start="{{ 'true' if auto_generation.can_auto_start else 'false' }}"
|
||||
|
||||
@@ -380,6 +380,9 @@ def test_ppt_audit_history_audit_rows_include_inline_replay(client, monkeypatch,
|
||||
assert 'data-ppt-open-preview' in html
|
||||
assert 'ocbot_daily_20260517.pptx?action=pdf' in html
|
||||
assert '圖表被切掉' in html
|
||||
assert '視覺問題追蹤' in html
|
||||
assert '版面越界' in html
|
||||
assert '問題回放 · ocbot_daily_20260517.pptx' in html
|
||||
assert '回放' in html
|
||||
|
||||
|
||||
|
||||
@@ -451,6 +451,89 @@ body.ppt-preview-open {
|
||||
box-shadow: var(--momo-shadow-md, 0 16px 38px rgba(70, 46, 28, 0.08));
|
||||
}
|
||||
|
||||
.ppt-issue-board {
|
||||
margin-top: var(--momo-space-4, 16px);
|
||||
padding: var(--momo-space-4, 16px);
|
||||
border: 1px solid var(--obs-line);
|
||||
border-radius: var(--momo-radius-lg, 8px);
|
||||
background:
|
||||
radial-gradient(circle, rgba(45, 40, 32, 0.08) 1px, transparent 1.2px),
|
||||
rgba(255, 250, 242, 0.58);
|
||||
background-size: 10px 10px, auto;
|
||||
box-shadow: var(--momo-shadow-md, 0 16px 38px rgba(70, 46, 28, 0.08));
|
||||
}
|
||||
|
||||
.ppt-issue-metrics {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--momo-space-2, 8px);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ppt-issue-metrics span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--momo-space-1, 4px);
|
||||
min-height: 30px;
|
||||
padding: 0 var(--momo-space-2, 8px);
|
||||
border: 1px solid rgba(86, 64, 48, 0.14);
|
||||
border-radius: 999px;
|
||||
color: var(--obs-muted);
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
font-size: var(--momo-text-caption, 12px);
|
||||
}
|
||||
|
||||
.ppt-issue-metrics strong {
|
||||
color: var(--obs-ink);
|
||||
font-family: var(--momo-font-mono);
|
||||
}
|
||||
|
||||
.ppt-issue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--momo-space-3, 12px);
|
||||
}
|
||||
|
||||
.ppt-issue-card {
|
||||
display: grid;
|
||||
align-content: space-between;
|
||||
min-height: 196px;
|
||||
padding: var(--momo-space-3, 12px);
|
||||
border: 1px solid var(--obs-line);
|
||||
border-top: 4px solid var(--obs-amber);
|
||||
border-radius: var(--momo-radius-lg, 8px);
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.ppt-issue-card.is-error {
|
||||
border-top-color: var(--obs-red);
|
||||
}
|
||||
|
||||
.ppt-issue-card strong {
|
||||
display: block;
|
||||
margin-top: var(--momo-space-2, 8px);
|
||||
color: var(--obs-ink);
|
||||
font-size: var(--momo-text-body, 14px);
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ppt-issue-card p {
|
||||
margin: var(--momo-space-2, 8px) 0 0;
|
||||
color: var(--obs-muted);
|
||||
font-size: var(--momo-text-body, 14px);
|
||||
line-height: 1.55;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.ppt-issue-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--momo-space-2, 8px);
|
||||
}
|
||||
|
||||
.ppt-action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
@@ -882,6 +965,10 @@ body.ppt-preview-open {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.ppt-issue-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.ppt-deck-rail {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -902,6 +989,7 @@ body.ppt-preview-open {
|
||||
.ppt-deck-rail,
|
||||
.ppt-stage-grid,
|
||||
.ppt-action-grid,
|
||||
.ppt-issue-grid,
|
||||
.ppt-coverage-score,
|
||||
.ppt-coverage-list {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -912,6 +1000,7 @@ body.ppt-preview-open {
|
||||
.ppt-table-title,
|
||||
.ppt-workbench-head,
|
||||
.ppt-workbench-actions,
|
||||
.ppt-issue-metrics,
|
||||
.ppt-preview-head,
|
||||
.ppt-run-log-head,
|
||||
.ppt-run-row,
|
||||
|
||||
Reference in New Issue
Block a user