quality_rules.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """
  2. Quality inspection rule definitions for PPT quality assurance.
  3. Each rule defines a check function, severity level, and auto-fix strategy.
  4. """
  5. from pptx.util import Emu, Pt
  6. from dataclasses import dataclass, field
  7. from typing import Callable, Optional
  8. @dataclass
  9. class QualityRule:
  10. rule_id: str
  11. category: str
  12. description: str
  13. severity: str
  14. auto_fixable: bool
  15. check_fn: str
  16. fix_fn: str
  17. QUALITY_RULES = [
  18. QualityRule('L001', 'layout', '元素飞出页面左边界', 'critical', True, '_check_left_bounds', '_fix_left_bounds'),
  19. QualityRule('L002', 'layout', '元素飞出页面右边界', 'critical', True, '_check_right_bounds', '_fix_right_bounds'),
  20. QualityRule('L003', 'layout', '元素飞出页面顶部', 'critical', True, '_check_top_bounds', '_fix_top_bounds'),
  21. QualityRule('L004', 'layout', '元素飞出页面底部', 'critical', True, '_check_bottom_bounds', '_fix_bottom_bounds'),
  22. QualityRule('L005', 'layout', '图文重叠', 'critical', True, '_check_overlap', '_fix_overlap'),
  23. QualityRule('L006', 'layout', '占位符未替换', 'critical', True, '_check_placeholders', '_fix_placeholders'),
  24. QualityRule('L007', 'layout', '元素紧贴页面边缘', 'minor', True, '_check_edge_proximity', '_fix_edge_proximity'),
  25. QualityRule('V001', 'visual', '字体不一致', 'minor', True, '_check_font_consistency', '_fix_font_consistency'),
  26. QualityRule('V002', 'visual', '字号过小(<8pt)', 'major', True, '_check_font_too_small', '_fix_font_too_small'),
  27. QualityRule('V003', 'visual', '字号过大(>60pt)', 'major', True, '_check_font_too_large', '_fix_font_too_large'),
  28. QualityRule('V004', 'visual', '颜色对比度不足', 'major', True, '_check_contrast', '_fix_contrast'),
  29. QualityRule('V005', 'visual', '图片拉伸变形', 'major', True, '_check_image_aspect', '_fix_image_aspect'),
  30. QualityRule('C001', 'content', '页面留白过多(填充率<35%)', 'critical', True, '_check_sparse_page', '_fix_sparse_page'),
  31. QualityRule('C002', 'content', 'KPI卡片数值为空', 'critical', True, '_check_empty_kpi', '_fix_empty_kpi'),
  32. QualityRule('C003', 'content', '图表无数据', 'critical', True, '_check_empty_chart', '_fix_empty_chart'),
  33. QualityRule('C004', 'content', '文本截断溢出', 'major', True, '_check_text_overflow', '_fix_text_overflow'),
  34. QualityRule('C005', 'content', '分析文本过短(<100字)', 'critical', True, '_check_short_text', '_fix_short_text'),
  35. QualityRule('C006', 'content', '页面缺少标题', 'critical', True, '_check_missing_title', '_fix_missing_title'),
  36. QualityRule('C007', 'content', '分析段数不足', 'critical', True, '_check_insight_count', '_fix_insight_count'),
  37. QualityRule('C008', 'content', '页面内容为空(<50字)', 'critical', True, '_check_empty_page', '_fix_empty_page'),
  38. QualityRule('C009', 'content', '图表缺少分析文本', 'critical', True, '_check_chart_no_text', '_fix_chart_no_text'),
  39. QualityRule('D001', 'data', '图表数据与文本矛盾', 'critical', False, '_check_data_text_contradiction', None),
  40. QualityRule('D002', 'data', '页码错乱', 'major', True, '_check_page_numbers', '_fix_page_numbers'),
  41. QualityRule('D003', 'data', '数据来源缺失', 'major', True, '_check_missing_source', '_fix_missing_source'),
  42. QualityRule('D004', 'data', '表格列宽不合理', 'minor', True, '_check_table_column_width', '_fix_table_column_width'),
  43. QualityRule('D005', 'data', '图表刻度异常', 'minor', True, '_check_axis_scale', '_fix_axis_scale'),
  44. ]
  45. QUALITY_RULES.extend([
  46. QualityRule('L008', 'layout', '空模板组件残留', 'major', True,
  47. '_check_empty_template_artifacts', '_fix_empty_template_artifacts'),
  48. QualityRule('C010', 'content', '动态页面与数据画像不匹配', 'critical', True,
  49. '_check_dynamic_page_fit', '_fix_rebuild_page'),
  50. QualityRule('C011', 'content', 'KPI布局容量不足', 'major', True,
  51. '_check_kpi_layout_capacity', '_fix_rebuild_page'),
  52. QualityRule('D006', 'data', '六项确认与输出不一致', 'critical', True,
  53. '_check_confirmation_alignment', '_fix_rebuild_page'),
  54. ])
  55. SEVERITY_WEIGHTS = {
  56. 'critical': 20,
  57. 'major': 10,
  58. 'minor': 3,
  59. }
  60. CATEGORY_WEIGHTS = {
  61. 'layout': 0.30,
  62. 'visual': 0.25,
  63. 'content': 0.25,
  64. 'data': 0.20,
  65. }
  66. FILL_RATIO_THRESHOLDS = {
  67. 'sparse': 0.20,
  68. 'low': 0.35,
  69. 'acceptable': 0.55,
  70. 'good': 0.70,
  71. }
  72. FONT_SIZE_MIN = Pt(8)
  73. FONT_SIZE_MAX = Pt(60)
  74. TEXT_MIN_LENGTH = 80
  75. INSIGHT_MIN_COUNT = 2
  76. PAGE_MIN_TEXT_LENGTH = 50
  77. SAFE_MARGIN = Emu(762000)
  78. CONTENT_LEFT = Emu(762000)
  79. CONTENT_TOP_BASE = Emu(1524000)
  80. FOOTER_TOP = Emu(8824000)
  81. SLIDE_WIDTH = 16256000
  82. SLIDE_HEIGHT = 9144000
  83. DEFAULT_FONT = '微软雅黑'
  84. DEFAULT_NUMBER_FONT = 'Arial'
  85. def get_rules_by_category(category: str) -> list[QualityRule]:
  86. return [r for r in QUALITY_RULES if r.category == category]
  87. def get_rules_by_severity(severity: str) -> list[QualityRule]:
  88. return [r for r in QUALITY_RULES if r.severity == severity]
  89. def calculate_score(issues_by_severity: dict, issues_by_category: dict, total_pages: int) -> int:
  90. if total_pages <= 0:
  91. return 100
  92. penalty = 0
  93. for sev, count in issues_by_severity.items():
  94. weight = SEVERITY_WEIGHTS.get(sev, 5)
  95. penalty += count * weight
  96. per_page_penalty = min(penalty / total_pages, 80)
  97. score = max(0, 100 - per_page_penalty)
  98. return int(score)
  99. def get_quality_label(score: int) -> str:
  100. if score >= 90:
  101. return '优质'
  102. elif score >= 75:
  103. return '良好'
  104. elif score >= 60:
  105. return '待改善'
  106. else:
  107. return '不合格'
  108. if __name__ == '__main__':
  109. print(f"Loaded {len(QUALITY_RULES)} quality rules")
  110. for cat in ['layout', 'visual', 'content', 'data']:
  111. rules = get_rules_by_category(cat)
  112. print(f" {cat}: {len(rules)} rules")
  113. for sev in ['critical', 'major', 'minor']:
  114. rules = get_rules_by_severity(sev)
  115. print(f" {sev}: {len(rules)} rules")