generate-demo-extension.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. """Deterministic fictional industry data; no real users, scraped quotes or production IO."""
  2. import hashlib
  3. import json
  4. from pathlib import Path
  5. ROOT = Path(__file__).resolve().parents[1]
  6. seed = json.loads((ROOT / 'data/preview-seed.json').read_text(encoding='utf-8'))
  7. DEMO = seed['groups'][0]['demoId']
  8. COMMON = dict(demoId=DEMO, dataMode='sample', isSynthetic=True, provenanceType='synthetic_demo')
  9. ext = dict(version='ksf.synthetic-extension.20261103.v2', demoId=DEMO, isSynthetic=True,
  10. note='全部为规则生成的虚构观察;不是实际访谈、商品报价、销量或营养检测。',
  11. voices=[], observations=[], prices=[], citations=[], researchTasks=[], collaborationRecords=[])
  12. contexts = [
  13. ['工位午餐', '加班后在茶水间', '通勤前的早餐', '午休只剩十五分钟', '下午临时加餐', '晚归后独自吃饭'],
  14. ['宿舍晚自习后', '考试周的午饭', '同寝分享夜宵', '周末留校晚饭', '图书馆回宿舍', '月末控制餐费'],
  15. ['有炉具的露营', '只有保温水壶的徒步', '长途高铁上', '服务区停留', '雨天帐篷里', '出差住酒店'],
  16. ['运动后便餐', '午间轻食', '晚间控制总能量', '轮班后的早餐', '通勤时补一餐', '周末在家吃面'],
  17. ['两人晚餐', '独自加班后回家', '孩子睡后的夜宵', '家庭周末午饭', '冰箱剩菜搭配', '临时替代外卖'],
  18. ['第一次尝红酸汤', '连续第二次吃地域风味', '朋友推荐的椒麻面', '周末尝新', '节前囤几种口味', '与室友分吃新口味'],
  19. ]
  20. purposes = ['这顿当正餐', '只是两餐之间垫一下', '想和同伴分着吃', '主要想喝热汤', '只是嘴馋试味道', '晚饭之后又有点饿']
  21. tools = ['手边只有热水,没有锅', '可以用电煮锅,愿意多等三分钟', '微波炉要排队,暂时不考虑', '需要倒水的做法不方便', '有锅但不想洗太多餐具', '有小炉子但用水有限']
  22. tastes = ['喜欢葱香但不想味道留在房间', '想要番茄酸味,不喜欢甜腻', '希望辣油单独放,可以自己决定', '更在意面条有嚼劲', '喜欢浓酱,清淡不是我的首选', '汤鲜就行,不追求地名听起来正宗']
  23. behaviors = ['我平时就加鸡蛋,不代表这份面不够', '这次吃一份刚好,不打算再加主食', '如果是加餐,我可能只吃半份', '愿意配蔬菜是为了换口感', '偶尔会再买饭团,要先区分是不是没吃正餐', '包装里有没有碗,比多一包酱更影响我选择']
  24. budgets = ['每份五元以内才会常买', '七元上下可以接受,要把运费算进去', '愿意多花两元换方便的制备', '组合装先算清楚每顿多少钱', '促销会囤,平时按需要买', '价格不是第一位,吃得方便更重要']
  25. frequencies = ['每周大约一次', '一个月两三次', '只在赶时间时', '最近想换口味时', '出差或值班才', '和朋友一起时']
  26. channels = ['模拟社媒分享', '模拟电商评价', '模拟追问记录', '模拟使用日记']
  27. for group_index, group in enumerate(seed['groups']):
  28. group_projects = [p for p in seed['projects'] if p['groupId'] == group['id']]
  29. for i in range(2000):
  30. # A coprime permutation spreads all dimensions rather than repeating six stock quotes.
  31. code = (i * 7919 + group_index * 137) % (6 ** 7)
  32. digits = []
  33. for _ in range(7):
  34. digits.append(code % 6); code //= 6
  35. a, b, c, d, e, f, g = digits
  36. raw = f'{contexts[group_index][a]},{purposes[b]}。{tools[c]};{tastes[d]}。{behaviors[e]}。{budgets[f]},我{frequencies[g]}会考虑方便面。'
  37. project = group_projects[i % 2]
  38. ext['voices'].append(dict(COMMON, id=f'SIM-VOICE-{group_index+1}-{i+1:04d}', groupId=group['id'], projectId=project['id'],
  39. title=f'{contexts[group_index][a]}|{purposes[b]}', rawText=raw, channel=channels[i % 4],
  40. authorLabel=f'模拟观察{group_index+1}-{i+1:04d}', verificationState='synthetic_not_real_interview',
  41. mealPurpose=purposes[b], preparation=tools[c], budget=budgets[f], behavior=behaviors[e],
  42. contentState=['相关分享', '相关分享', '广告待排除', '内容不全', '相关分享'][i % 5]))
  43. packaging = ['袋装', '碗装', '杯装', '双份组合', '家庭六连包']
  44. flavors = ['番茄鸡蛋', '葱油酱香', '菌菇清汤', '轻辣芝士', '红酸汤', '川香椒麻']
  45. for i in range(720):
  46. p = seed['projects'][i % 12]
  47. pack_count = [1, 1, 2, 6, None][i % 5]
  48. weight = [85, 95, 110, 125, None][(i // 5) % 5]
  49. obs = dict(COMMON, id=f'SIM-OBS-{i+1:04d}', groupId=p['groupId'], projectId=p['id'],
  50. title=f'模拟系列{(i//30)+1:02d} · {flavors[(i//5)%6]} · {packaging[i%5]}',
  51. productName=f'模拟系列{(i//30)+1:02d}', flavor=flavors[(i//5)%6], packaging=packaging[i%5],
  52. netWeightG=weight, packCount=pack_count, preparation=['热水复水候选', '需煮制候选', '条件待核'][i % 3],
  53. nutritionClaim=None, actualSku=None, note='虚构商品观察,未关联真实在售商品;营养与工艺未知。')
  54. ext['observations'].append(obs)
  55. for j, day in enumerate(['2026-10-12', '2026-10-19', '2026-10-26', '2026-11-02']):
  56. count = pack_count
  57. paid = round((4.5 + (i % 17) * .37 + j * .25) * (count or 1), 2)
  58. comparable = bool(count and weight and j != 2)
  59. ext['prices'].append(dict(COMMON, id=f'SIM-PRICE-{i+1:04d}-{j+1}', observationId=obs['id'], projectId=p['id'], groupId=p['groupId'],
  60. date=day, channel=['模拟电商自营', '模拟商超', '模拟促销组合', '模拟便利店'][j], currency='CNY',
  61. pricePaid=paid, includesShipping=j == 0, packCount=count, netWeightG=weight,
  62. unitPricePer100G=round(paid / (count * weight) * 100, 2) if comparable else None,
  63. comparisonState='可按演示规格比较' if comparable else '口径待核,不参与单位价比较'))
  64. for i in range(354):
  65. source = ext['voices'][(i * 31) % len(ext['voices'])]
  66. quote = source['rawText'].split(';')[0] + ';'
  67. ext['citations'].append(dict(COMMON, id=f'SIM-EVIDENCE-{i+1:03d}', groupId=source['groupId'], projectId=source['projectId'],
  68. sourceId=source['id'], quote=quote, locator=dict(start=0, end=len(quote)), reviewState='demo_only',
  69. stance=['support', 'contradict', 'unresolved'][i % 3], claim='用于演示不同场景与其他解释,并非已验证用户需求。'))
  70. for i in range(180):
  71. p = seed['projects'][i % 12]
  72. state = ['done', 'in_progress', 'pending', 'failed', 'reused'][i % 5]
  73. ext['researchTasks'].append(dict(COMMON, id=f'SIM-RESEARCH-{i+1:03d}', groupId=p['groupId'], projectId=p['id'],
  74. ownerMemberId=p['memberIds'][(i//12) % 6], scope=['核对面体制备说明', '区分正餐与加餐', '检查商品组合规格', '核对相反材料', '追问搭配原因'][i % 5],
  75. state=state, requiredMaterial=['包装制备原文', '带情境的追问记录', '规格与价格截图'][i % 3],
  76. failureReason='演示来源不可用,保留失败并待补其他来源' if state == 'failed' else None,
  77. note='模拟任务回放;没有执行真实采集。'))
  78. for i in range(312):
  79. p = seed['projects'][i % 12]
  80. owner = seed['members'][(i * 7) % 36]
  81. body = ['这条声音是否可能来自加餐?建议先问清楚当餐目的。', '已在模拟资料中补上制备条件,原方案版本保留。',
  82. '支持继续研究,但不能把搭配习惯直接当成份量不足。', '价格缺少包数,请先核规格,暂不参与单位价比较。',
  83. '工艺条件与目标场景冲突,建议保留暂停理由并分配补证任务。', '课堂方案可展示,实际成本和营养参数仍待核实。'][i % 6]
  84. ext['collaborationRecords'].append(dict(COMMON, id=f'SIM-COLLAB-{i+1:03d}', groupId=p['groupId'], projectId=p['id'],
  85. ownerMemberId=owner['id'], user=owner['displayName'], kind=['质疑', '答复', '补证', '版本说明', '取舍', '复核'][i % 6], body=body,
  86. at=f'2026-11-0{1+i%3}T{9+(i%8):02d}:{i%60:02d}:00+08:00', note='模拟协作记录'))
  87. counts = {key: len(ext[key]) for key in ['voices', 'observations', 'prices', 'citations', 'researchTasks', 'collaborationRecords']}
  88. assert len({v['rawText'] for v in ext['voices']}) == 12000
  89. assert sum(counts.values()) == 16446
  90. ext['counts'] = counts
  91. target = ROOT / 'src/assets/showcase/demo-extension.json'
  92. target.write_text(json.dumps(ext, ensure_ascii=False, separators=(',', ':')) + '\n', encoding='utf-8')
  93. print(json.dumps(dict(file=str(target), bytes=target.stat().st_size, counts=counts, uniqueVoices=12000,
  94. totalEvidence=len(seed['evidence'])+counts['citations'], sha256=hashlib.sha256(target.read_bytes()).hexdigest()), ensure_ascii=False))