designaround-engine 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- designaround_engine/__init__.py +9 -0
- designaround_engine/advisor.py +397 -0
- designaround_engine/api.py +1265 -0
- designaround_engine/baseline.py +142 -0
- designaround_engine/cad.py +160 -0
- designaround_engine/cad_worker.py +153 -0
- designaround_engine/camera.py +77 -0
- designaround_engine/compare.py +208 -0
- designaround_engine/concepts.py +175 -0
- designaround_engine/defense.py +477 -0
- designaround_engine/dfm.py +178 -0
- designaround_engine/errors.py +35 -0
- designaround_engine/evidence.py +89 -0
- designaround_engine/filing.py +312 -0
- designaround_engine/geometry.py +238 -0
- designaround_engine/hlr_worker.py +40 -0
- designaround_engine/lineart.py +395 -0
- designaround_engine/meshimport.py +656 -0
- designaround_engine/originality.py +119 -0
- designaround_engine/overlay.py +75 -0
- designaround_engine/params.py +147 -0
- designaround_engine/photos.py +90 -0
- designaround_engine/raster.py +99 -0
- designaround_engine/report.py +230 -0
- designaround_engine/risk.py +83 -0
- designaround_engine/rpc.py +110 -0
- designaround_engine/rules/dfm.yaml +19 -0
- designaround_engine/sample.py +60 -0
- designaround_engine/sandbox.py +159 -0
- designaround_engine/shading.py +245 -0
- designaround_engine/templates/__init__.py +57 -0
- designaround_engine/templates/storage_box.py +138 -0
- designaround_engine/templates/tumbler.py +94 -0
- designaround_engine/viewpack.py +435 -0
- designaround_engine/views.py +82 -0
- designaround_engine/watermark.py +156 -0
- designaround_engine/workspace.py +278 -0
- designaround_engine-0.1.3.dist-info/METADATA +18 -0
- designaround_engine-0.1.3.dist-info/RECORD +41 -0
- designaround_engine-0.1.3.dist-info/WHEEL +4 -0
- designaround_engine-0.1.3.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""DesignAround local engine.
|
|
2
|
+
|
|
3
|
+
Everything in this package runs on the user's machine (CLI, desktop app) or on a
|
|
4
|
+
server CPU worker. Nothing here needs a GPU: CAD programs are executed with
|
|
5
|
+
CadQuery/OpenCascade, patent views come from hidden-line removal, renders and
|
|
6
|
+
silhouettes come from a CPU rasteriser, and shape comparison is pure NumPy.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.3"
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Decision cards (FR-UX-02) and the "keep / redesign / abandon" overview (FR-UX-03).
|
|
2
|
+
|
|
3
|
+
A card is computed from the case's own records (concepts, versions, evaluations,
|
|
4
|
+
prior art, screens, filing packages, gates). It never calls a model, costs nothing
|
|
5
|
+
and is safe to show at any time. Wording rule: risk assessment, never a legal
|
|
6
|
+
conclusion; a registered practitioner confirms before filing or selling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from designaround_core import catalog
|
|
15
|
+
from designaround_core import quote as quote_mod
|
|
16
|
+
|
|
17
|
+
from . import concepts as concepts_mod
|
|
18
|
+
from .workspace import Case, open_case
|
|
19
|
+
|
|
20
|
+
DISCLAIMER = {
|
|
21
|
+
"zh": "风险评估,不是法律意见;上架或提交前须由执业专利代理师确认。",
|
|
22
|
+
"en": "Risk assessment only, not a legal opinion; a registered practitioner must confirm before you sell or file.",
|
|
23
|
+
}
|
|
24
|
+
LEVEL = {
|
|
25
|
+
"zh": {"high": "高", "medium": "中", "low": "低", "not_seen": "未见"},
|
|
26
|
+
"en": {"high": "high", "medium": "medium", "low": "low", "not_seen": "none seen"},
|
|
27
|
+
}
|
|
28
|
+
STAGES = ["start", "concepts", "build", "screen", "evaluate", "redesign", "file", "submit"]
|
|
29
|
+
STAGE_LABEL = {
|
|
30
|
+
"zh": {"start": "需求", "concepts": "设计方向", "build": "3D 建模", "screen": "检索", "evaluate": "比对评估", "redesign": "改型", "file": "申请材料", "submit": "提交"},
|
|
31
|
+
"en": {"start": "Requirements", "concepts": "Concepts", "build": "3D model", "screen": "Screening", "evaluate": "Assessment", "redesign": "Design-around", "file": "Filing package", "submit": "Submission"},
|
|
32
|
+
}
|
|
33
|
+
# Typical spend and time, from the price catalog and measured runs (2026-10).
|
|
34
|
+
TYPICAL = {
|
|
35
|
+
"concepts": {"credits": 25 * 4 + 40, "minutes": "2–4"},
|
|
36
|
+
"build": {"credits": 20, "max_credits": 3000, "minutes": "3–6"},
|
|
37
|
+
"screen_quick": {"credits": catalog.ACTION_CREDITS["screen.quick"], "minutes": "1–3"},
|
|
38
|
+
"screen_deep": {"credits": catalog.ACTION_CREDITS["screen.deep"], "minutes": "3–10"},
|
|
39
|
+
"observe": {"credits": 10, "minutes": "1–2"},
|
|
40
|
+
"around": {"credits": 1500, "max_credits": catalog.AROUND_DEPTHS["standard"]["max_credits"], "minutes": "20–40"},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _usd(credits: int) -> float:
|
|
45
|
+
return round(credits * catalog.CREDIT_USD, 2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _latest_screen(case: Case) -> dict | None:
|
|
49
|
+
folder = case.root / "screens"
|
|
50
|
+
files = sorted(folder.glob("*.json")) if folder.exists() else []
|
|
51
|
+
if not files:
|
|
52
|
+
return None
|
|
53
|
+
data = json.loads(files[-1].read_text(encoding="utf-8"))
|
|
54
|
+
return {
|
|
55
|
+
"file": str(files[-1]),
|
|
56
|
+
"at": files[-1].stem,
|
|
57
|
+
"candidates": len(data.get("candidates") or []),
|
|
58
|
+
"blind_spots": data.get("blind_spots") or [],
|
|
59
|
+
"source_failures": data.get("source_failures") or [],
|
|
60
|
+
"queries": data.get("queries") or [],
|
|
61
|
+
"jurisdictions": data.get("jurisdictions") or case.meta.get("jurisdictions") or [],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _evaluation(case: Case, vid: str | None) -> dict | None:
|
|
66
|
+
if not vid:
|
|
67
|
+
return None
|
|
68
|
+
path = case.evaluation_path(vid)
|
|
69
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _filings(case: Case) -> list[dict]:
|
|
73
|
+
root = case.root / "filings"
|
|
74
|
+
rows = []
|
|
75
|
+
if root.exists():
|
|
76
|
+
for package in sorted(root.glob("*/*/package")):
|
|
77
|
+
office, vid = package.parent.parent.name, package.parent.name
|
|
78
|
+
checklist = package / "checklist.json"
|
|
79
|
+
status = json.loads(checklist.read_text(encoding="utf-8")).get("status") if checklist.exists() else None
|
|
80
|
+
rows.append({"office": office, "version": vid, "dir": str(package), "status": status})
|
|
81
|
+
return rows
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _events(case: Case, kind: str) -> list[dict]:
|
|
85
|
+
return [e for e in case.log.read() if e.get("type") == kind]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def facts(case_id: str) -> dict:
|
|
89
|
+
"""Everything the card and the report need, gathered once."""
|
|
90
|
+
|
|
91
|
+
case = open_case(case_id)
|
|
92
|
+
meta = case.meta
|
|
93
|
+
versions = case.versions()
|
|
94
|
+
original = case.original_version()
|
|
95
|
+
concept_rows = concepts_mod.concepts(case_id)
|
|
96
|
+
evaluation = _evaluation(case, original)
|
|
97
|
+
candidates = []
|
|
98
|
+
for v in versions:
|
|
99
|
+
if v.get("kind") != "candidate":
|
|
100
|
+
continue
|
|
101
|
+
ev = _evaluation(case, v["id"])
|
|
102
|
+
candidates.append({"id": v["id"], "label": v.get("label"), "risk": v.get("risk"), "level": v.get("risk_level"), "qualified": bool(v.get("qualified")), "originality": (v.get("originality") or {}).get("pass"), "evaluated": ev is not None})
|
|
103
|
+
spent = sum(int((e.get("payload") or {}).get("charged") or 0) for e in _events(case, "screen"))
|
|
104
|
+
return {
|
|
105
|
+
"case": case.id,
|
|
106
|
+
"name": meta.get("name"),
|
|
107
|
+
"article": meta.get("article_zh") or meta.get("article"),
|
|
108
|
+
"jurisdictions": meta.get("jurisdictions") or [],
|
|
109
|
+
"brief": meta.get("brief") or {},
|
|
110
|
+
"photos": bool((case.root / "photos").exists() and any((case.root / "photos").iterdir())),
|
|
111
|
+
"concepts": [{"id": c["id"], "title": c.get("title"), "pass": (c.get("review") or {}).get("pass")} for c in concept_rows],
|
|
112
|
+
"chosen_concept": meta.get("chosen_concept"),
|
|
113
|
+
"original": original,
|
|
114
|
+
"drafts": [{"id": v["id"], "match": (v.get("review") or {}).get("match")} for v in versions if v.get("kind") == "draft"],
|
|
115
|
+
"candidates": candidates,
|
|
116
|
+
"prior_art": len(case.prior_art()),
|
|
117
|
+
"prior_art_with_figures": sum(1 for p in case.prior_art() if p.get("figures")),
|
|
118
|
+
"screen": _latest_screen(case),
|
|
119
|
+
"evaluation": evaluation,
|
|
120
|
+
"filings": _filings(case),
|
|
121
|
+
"gates": [e.get("payload") or {} for e in _events(case, "gate.created")],
|
|
122
|
+
"baselines": [e.get("payload") or {} for e in _events(case, "baseline.frozen")],
|
|
123
|
+
"disclosures": case.disclosures(),
|
|
124
|
+
"screen_credits_spent": spent,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _filing_quote(offices: list[str], managed: bool = False) -> dict:
|
|
129
|
+
offices = [o for o in offices if o in ("US", "CN", "EU")] or ["US"]
|
|
130
|
+
return quote_mod.filing_quote(offices, managed=managed)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _managed(offices: list[str], redesign: bool) -> dict | None:
|
|
134
|
+
key = {
|
|
135
|
+
(True, ("CN",)): "managed_cn_around_file",
|
|
136
|
+
(True, ("US",)): "managed_us_around_file",
|
|
137
|
+
(True, ("CN", "US")): "managed_cn_us_around_file",
|
|
138
|
+
(False, ("CN",)): "managed_cn_file_only",
|
|
139
|
+
(False, ("US",)): "managed_us_file_only",
|
|
140
|
+
}.get((redesign, tuple(sorted(o for o in offices if o in ("US", "CN")))))
|
|
141
|
+
if not key:
|
|
142
|
+
return None
|
|
143
|
+
pkg = catalog.MANAGED_PACKAGES[key]
|
|
144
|
+
return {"package": key, "platform_usd": pkg["platform_usd"], "practitioner_usd_est": pkg["practitioner_usd_est"], "official_fees": "extra"}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _step(lang: str, label_zh: str, label_en: str, *, command: str | None = None, prompt_zh: str | None = None, prompt_en: str | None = None, credits: int | None = None, max_credits: int | None = None, minutes: str | None = None, free: bool = False) -> dict:
|
|
148
|
+
step = {"label": label_zh if lang == "zh" else label_en, "command": command, "prompt": (prompt_zh if lang == "zh" else prompt_en) or (label_zh if lang == "zh" else label_en)}
|
|
149
|
+
if credits is not None:
|
|
150
|
+
step["credits"] = credits
|
|
151
|
+
step["usd"] = _usd(credits)
|
|
152
|
+
if max_credits is not None:
|
|
153
|
+
step["max_credits"] = max_credits
|
|
154
|
+
if minutes:
|
|
155
|
+
step["minutes"] = minutes
|
|
156
|
+
if free:
|
|
157
|
+
step["free"] = True
|
|
158
|
+
return step
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _risk_reasons(f: dict, lang: str, limit: int = 3) -> list[str]:
|
|
162
|
+
ev = f.get("evaluation") or {}
|
|
163
|
+
# Observer-scored rights decide; outline-only rows are listed after them.
|
|
164
|
+
rows = sorted(ev.get("per_prior_art") or [], key=lambda r: (r.get("basis") != "shape+observer", -r.get("risk", 0)))
|
|
165
|
+
out = []
|
|
166
|
+
for r in rows[:limit]:
|
|
167
|
+
obs = r.get("observer") or {}
|
|
168
|
+
similar = ";".join(obs.get("similar") or [])[:120] if lang == "zh" else "; ".join(obs.get("similar") or [])[:160]
|
|
169
|
+
lvl = LEVEL[lang].get(r.get("level"), r.get("level"))
|
|
170
|
+
if lang == "zh":
|
|
171
|
+
text = f"{r.get('number') or r.get('prior_art_id')}:风险 {r.get('risk', 0):.2f}({lvl})" + (f",观察者 {obs.get('score')}/10" if obs.get("score") is not None else ",只有轮廓分") + (f";相同点:{similar}" if similar else "")
|
|
172
|
+
else:
|
|
173
|
+
text = f"{r.get('number') or r.get('prior_art_id')}: risk {r.get('risk', 0):.2f} ({lvl})" + (f", observer {obs.get('score')}/10" if obs.get("score") is not None else ", outline score only") + (f"; shared: {similar}" if similar else "")
|
|
174
|
+
out.append(text)
|
|
175
|
+
return out
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _options(f: dict, lang: str, *, best_candidate: dict | None) -> list[dict]:
|
|
179
|
+
"""FR-UX-03: keep as is / redesign / abandon, side by side."""
|
|
180
|
+
|
|
181
|
+
offices = f["jurisdictions"] or ["US"]
|
|
182
|
+
q = _filing_quote(offices)
|
|
183
|
+
fees = round(q["total_usd_estimate"], 0)
|
|
184
|
+
ev = f.get("evaluation") or {}
|
|
185
|
+
summary = ev.get("summary") or {}
|
|
186
|
+
level = LEVEL[lang].get(summary.get("level"), summary.get("level"))
|
|
187
|
+
around = TYPICAL["around"]
|
|
188
|
+
zh = lang == "zh"
|
|
189
|
+
keep = {
|
|
190
|
+
"key": "keep",
|
|
191
|
+
"label": "不改,直接推进" if zh else "Keep the design",
|
|
192
|
+
"risk": f"{summary.get('max_risk', 0):.2f}({level})" if zh else f"{summary.get('max_risk', 0):.2f} ({level})",
|
|
193
|
+
"platform_credits": 0,
|
|
194
|
+
"filing_usd_estimate": fees,
|
|
195
|
+
"minutes": None,
|
|
196
|
+
"note": "风险不变;适合风险低的情况" if zh else "Risk stays as assessed; suits low-risk results",
|
|
197
|
+
}
|
|
198
|
+
redesign = {
|
|
199
|
+
"key": "redesign",
|
|
200
|
+
"label": "改型后推进" if zh else "Design around first",
|
|
201
|
+
"risk": (f"目标 < {summary.get('threshold', 0.4):.2f}" if zh else f"target < {summary.get('threshold', 0.4):.2f}") if not best_candidate or best_candidate.get("risk") is None else (f"{best_candidate['id']} 已到 {best_candidate['risk']:.2f}" if zh else f"{best_candidate['id']} reached {best_candidate['risk']:.2f}"),
|
|
202
|
+
"platform_credits": around["credits"],
|
|
203
|
+
"platform_usd": _usd(around["credits"]),
|
|
204
|
+
"max_credits": around["max_credits"],
|
|
205
|
+
"filing_usd_estimate": fees,
|
|
206
|
+
"minutes": around["minutes"],
|
|
207
|
+
"note": "另需评估改模成本;标准或深度改型没有任何达标方案时,自动退回实际花费的 50%" if zh else "Mould changes to be assessed; a standard or deep run with no qualified candidate refunds 50% of its spend automatically",
|
|
208
|
+
}
|
|
209
|
+
abandon = {
|
|
210
|
+
"key": "abandon",
|
|
211
|
+
"label": "放弃这个款" if zh else "Drop the product",
|
|
212
|
+
"risk": "无" if zh else "none",
|
|
213
|
+
"platform_credits": 0,
|
|
214
|
+
"filing_usd_estimate": 0,
|
|
215
|
+
"minutes": None,
|
|
216
|
+
"note": (f"已花费检索 {f['screen_credits_spent']} 积分" if zh else f"Spent so far on screening: {f['screen_credits_spent']} credits"),
|
|
217
|
+
}
|
|
218
|
+
return [keep, redesign, abandon]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _next_deadline(case_id: str) -> dict | None:
|
|
222
|
+
"""The next hard deadline from recorded filings, notices and disclosures (FR-FILE-12)."""
|
|
223
|
+
|
|
224
|
+
from datetime import date
|
|
225
|
+
|
|
226
|
+
from designaround_core import deadlines as dl
|
|
227
|
+
|
|
228
|
+
from . import api
|
|
229
|
+
|
|
230
|
+
case = open_case(case_id)
|
|
231
|
+
data = api.prosecution(case_id)
|
|
232
|
+
disclosures = case.disclosures()
|
|
233
|
+
matters = api._matters(case)
|
|
234
|
+
if not (data["filings"] or data["notices"] or disclosures or matters):
|
|
235
|
+
return None
|
|
236
|
+
today = date.today()
|
|
237
|
+
for d in dl.compute(offices=case.meta.get("jurisdictions") or [], filings=data["filings"], notices=data["notices"], disclosures=disclosures, matters=matters):
|
|
238
|
+
if d.hard and d.due >= today:
|
|
239
|
+
return d.to_dict(today)
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def decision_card(case_id: str, *, lang: str = "zh") -> dict:
|
|
244
|
+
"""One sentence verdict, reasons, next steps with cost and time, and the managed option."""
|
|
245
|
+
|
|
246
|
+
lang = "en" if lang == "en" else "zh"
|
|
247
|
+
zh = lang == "zh"
|
|
248
|
+
f = facts(case_id)
|
|
249
|
+
case = f["case"]
|
|
250
|
+
offices = f["jurisdictions"] or ["US"]
|
|
251
|
+
card: dict = {"case": case, "lang": lang, "reasons": [], "next": [], "options": None, "managed": None, "blind_spots": [], "wording": DISCLAIMER[lang]}
|
|
252
|
+
|
|
253
|
+
def done(stage: str, verdict: str, headline: str, summary: str) -> dict:
|
|
254
|
+
card.update({"stage": stage, "stage_label": STAGE_LABEL[lang][stage], "stage_index": STAGES.index(stage), "stages": [STAGE_LABEL[lang][s] for s in STAGES], "verdict": verdict, "headline": headline, "summary": summary})
|
|
255
|
+
if f.get("screen"):
|
|
256
|
+
card["blind_spots"] = f["screen"]["blind_spots"][:6]
|
|
257
|
+
card["screened_at"] = f["screen"]["at"]
|
|
258
|
+
nd = _next_deadline(case)
|
|
259
|
+
if nd:
|
|
260
|
+
card["deadline"] = nd
|
|
261
|
+
if nd["days_left"] <= 30:
|
|
262
|
+
card["reasons"].insert(0, f"期限提醒:{nd['title_zh']},{nd['due']}(还有 {nd['days_left']} 天)" if zh else f"Deadline: {nd['title_en']} on {nd['due']} ({nd['days_left']} days left)")
|
|
263
|
+
return card
|
|
264
|
+
|
|
265
|
+
original = f["original"]
|
|
266
|
+
if not original:
|
|
267
|
+
if f["drafts"]:
|
|
268
|
+
d = f["drafts"][-1]
|
|
269
|
+
card["reasons"].append(f"3D 草稿 {d['id']} 与概念图一致度 {d['match']}/10,没有通过独立审核" if zh else f"3D draft {d['id']} matched the concept {d['match']}/10 and did not pass the independent review")
|
|
270
|
+
card["next"] += [
|
|
271
|
+
_step(lang, "重新建模", "Build again", command=f"designaround design build {case} --max-credits {TYPICAL['build']['max_credits']}", prompt_zh="再建一次 3D 模型", prompt_en="Build the 3D model again", credits=TYPICAL["build"]["credits"], max_credits=TYPICAL["build"]["max_credits"], minutes=TYPICAL["build"]["minutes"]),
|
|
272
|
+
_step(lang, f"我确认采用草稿 {d['id']}", f"Accept draft {d['id']} myself", command=f'designaround design accept {case} {d["id"]} --person "<name>"', prompt_zh=f"我看过了,采用草稿 {d['id']} 作为原始设计", prompt_en=f"I reviewed it; use draft {d['id']} as the original design", free=True),
|
|
273
|
+
]
|
|
274
|
+
return done("build", "pending", "3D 草稿等你确认" if zh else "3D draft needs your decision", "建模没有通过审核,可以重建、简化方向,或你确认后采用。" if zh else "The build did not pass review: build again, simplify the concept, or accept the draft yourself.")
|
|
275
|
+
if f["chosen_concept"]:
|
|
276
|
+
card["next"].append(_step(lang, f"把方向 {f['chosen_concept']} 建成 3D", f"Build concept {f['chosen_concept']} in 3D", command=f"designaround design build {case} --max-credits {TYPICAL['build']['max_credits']}", prompt_zh=f"把方向 {f['chosen_concept']} 建成 3D 模型", prompt_en=f"Build concept {f['chosen_concept']} in 3D", credits=TYPICAL["build"]["credits"], max_credits=TYPICAL["build"]["max_credits"], minutes=TYPICAL["build"]["minutes"]))
|
|
277
|
+
return done("build", "pending", "已选定方向,下一步建 3D" if zh else "Concept chosen; build it in 3D", "CAD Agent 会从零建参数化模型,独立审核对照概念图。" if zh else "The CAD Agent builds a parametric model; an independent reviewer compares it with the concept.")
|
|
278
|
+
passed = [c for c in f["concepts"] if c.get("pass")]
|
|
279
|
+
if passed:
|
|
280
|
+
card["reasons"].append(("通过审核的方向:" if zh else "Concepts that passed review: ") + ", ".join(f"{c['id']} {c['title']}" for c in passed[:6]))
|
|
281
|
+
card["next"] += [
|
|
282
|
+
_step(lang, "选一个方向", "Choose a concept", command=f'designaround design choose {case} <concept> --person "<name>"', prompt_zh="我选这个方向:", prompt_en="I choose this concept: ", free=True),
|
|
283
|
+
_step(lang, "按我的意见改一个方向", "Refine a concept with my feedback", command=f'designaround design concepts {case} --base <concept> --feedback "..." --max-credits 300', prompt_zh="把这个方向改一下:", prompt_en="Refine this concept: ", credits=90, minutes="2–3"),
|
|
284
|
+
]
|
|
285
|
+
return done("concepts", "pending", "挑一个设计方向" if zh else "Pick a design direction", "看概念图,选一个,或者说要改哪里。" if zh else "Look at the concepts and pick one, or say what to change.")
|
|
286
|
+
if f["brief"] or f["concepts"]:
|
|
287
|
+
card["next"].append(_step(lang, "生成 4 个设计方向", "Generate 4 design directions", command=f"designaround design concepts {case} --count 4 --max-credits 680", prompt_zh="按我的需求出 4 个设计方向", prompt_en="Generate 4 design directions from my requirements", credits=TYPICAL["concepts"]["credits"], max_credits=680, minutes=TYPICAL["concepts"]["minutes"]))
|
|
288
|
+
return done("concepts", "pending", "需求已记下,下一步出设计方向" if zh else "Requirements recorded; next, design directions", "每张概念图都由独立审核员检查。" if zh else "Every concept image is checked by an independent reviewer.")
|
|
289
|
+
if f["photos"]:
|
|
290
|
+
card["next"].append(_step(lang, "按照片建 3D 样品", "Build the 3D sample from the photos", command=f"designaround model {case} --agent --known length=<mm> --max-credits 1500", prompt_zh="按照片建 3D 样品,已知尺寸:", prompt_en="Build the 3D sample from the photos; known size: ", credits=30, max_credits=1500, minutes="3–6"))
|
|
291
|
+
return done("build", "pending", "照片已上传,下一步建样品" if zh else "Photos uploaded; next, the 3D sample", "需要一个真实尺寸,例如长度。" if zh else "One real dimension is needed, for example the length.")
|
|
292
|
+
card["next"] += [
|
|
293
|
+
_step(lang, "我有一个产品想法", "I have a product idea", prompt_zh="我想做一款新产品:", prompt_en="I want to design a new product: ", free=True),
|
|
294
|
+
_step(lang, "我有照片或 CAD 文件", "I have photos or a CAD file", prompt_zh="我上传了产品照片/CAD 文件,帮我查风险", prompt_en="I uploaded photos / a CAD file; check the risk", free=True),
|
|
295
|
+
]
|
|
296
|
+
return done("start", "pending", "先告诉我你手上有什么" if zh else "Tell me what you have", "想法、照片、STEP/STL 文件,或收到的专利号都可以。" if zh else "An idea, photos, a STEP/STL file or a patent number all work.")
|
|
297
|
+
|
|
298
|
+
# From here the case has an original design (version) to assess.
|
|
299
|
+
if not f["screen"] and not f["prior_art"]:
|
|
300
|
+
card["next"] += [
|
|
301
|
+
_step(lang, "深度检索(以图搜图)", "Deep screening (image search)", command=f"designaround screen {case} --deep --max-credits 400", prompt_zh="帮我做深度外观专利检索", prompt_en="Run a deep design-patent screening", credits=TYPICAL["screen_deep"]["credits"], minutes=TYPICAL["screen_deep"]["minutes"]),
|
|
302
|
+
_step(lang, "快速检索(每月前 5 次免费)", "Quick screening (5 free a month)", command=f"designaround screen {case} --max-credits 60", prompt_zh="先做一次快速检索", prompt_en="Run a quick screening first", credits=TYPICAL["screen_quick"]["credits"], minutes=TYPICAL["screen_quick"]["minutes"]),
|
|
303
|
+
]
|
|
304
|
+
return done("screen", "pending", "设计已就绪,下一步检索" if zh else "Design ready; next, screening", f"检索 {', '.join(offices)} 已公开的外观设计。" if zh else f"Search published designs in {', '.join(offices)}.")
|
|
305
|
+
|
|
306
|
+
ev = f["evaluation"]
|
|
307
|
+
summary = (ev or {}).get("summary") or {}
|
|
308
|
+
if not ev or summary.get("pending_observer"):
|
|
309
|
+
pending = summary.get("pending_observer") or []
|
|
310
|
+
card["reasons"].append((f"找到 {f['prior_art']} 件在先设计({f['prior_art_with_figures']} 件有图)" if zh else f"{f['prior_art']} prior designs found ({f['prior_art_with_figures']} with figures)") + ((f";待观察者评分:{', '.join(pending)}" if zh else f"; observer score pending: {', '.join(pending)}") if pending else ""))
|
|
311
|
+
command = f"designaround observe {case} {original} {pending[0]} --max-credits 120" if pending else f"designaround evaluate {case} {original}"
|
|
312
|
+
card["next"].append(_step(lang, "让独立观察者逐件比对", "Have the isolated observer compare the closest ones", command=command, prompt_zh="让观察者比对最像的几件,并给出风险", prompt_en="Have the observer compare the closest prior designs and give the risk", credits=TYPICAL["observe"]["credits"] * max(1, len(pending) or 3), minutes=TYPICAL["observe"]["minutes"]))
|
|
313
|
+
return done("evaluate", "pending", "检索完成,下一步比对" if zh else "Screening done; next, the comparison", "轮廓排序只用来挑候选,结论以观察者评分为准。" if zh else "Outline ranking only picks candidates; the observer score decides.")
|
|
314
|
+
|
|
315
|
+
level = summary.get("level")
|
|
316
|
+
qualified_candidates = sorted([c for c in f["candidates"] if c["qualified"] and c["originality"] is not False], key=lambda c: c["risk"] if c["risk"] is not None else 1)
|
|
317
|
+
best = qualified_candidates[0] if qualified_candidates else None
|
|
318
|
+
if best:
|
|
319
|
+
lvl = LEVEL[lang].get(best.get("level"), best.get("level"))
|
|
320
|
+
card["reasons"].append(f"改型方案 {best['id']}:最高风险 {best['risk']:.2f}({lvl}),已过原创性检查,低于合格线" if zh else f"Candidate {best['id']}: highest risk {best['risk']:.2f} ({lvl}), passed the originality check, below the threshold")
|
|
321
|
+
card["reasons"] += [("原款 " if zh else "Original ") + r for r in _risk_reasons(f, lang, limit=2)]
|
|
322
|
+
else:
|
|
323
|
+
card["reasons"] += _risk_reasons(f, lang)
|
|
324
|
+
card["options"] = _options(f, lang, best_candidate=best)
|
|
325
|
+
target = best["id"] if best else original
|
|
326
|
+
|
|
327
|
+
filed = [x for x in f["filings"] if x["version"] == target]
|
|
328
|
+
if filed:
|
|
329
|
+
card["reasons"].append((f"{target} 的申请材料已生成:" if zh else f"Filing package for {target}: ") + ", ".join(f"{x['office']} {x['status'] or ''}".strip() for x in filed))
|
|
330
|
+
gate_open = any((g.get("type") == "filing_submission") for g in f["gates"])
|
|
331
|
+
frozen = [b for b in f["baselines"] if b.get("version") == target]
|
|
332
|
+
if frozen:
|
|
333
|
+
card["reasons"].append(f"提交基线 {frozen[-1]['baseline']} 已冻结" if zh else f"Submission baseline {frozen[-1]['baseline']} frozen")
|
|
334
|
+
else:
|
|
335
|
+
card["next"].append(_step(lang, "冻结提交基线", "Freeze the submission baseline", command=f"designaround baseline freeze {case} {target} --office {filed[0]['office']}", prompt_zh=f"冻结 {target} 的提交基线", prompt_en=f"Freeze the submission baseline of {target}", free=True))
|
|
336
|
+
if not gate_open:
|
|
337
|
+
card["next"].append(_step(lang, "交给执业代理师审核提交", "Send to a registered practitioner", command=f"designaround gate create {case} --type filing_submission --reviewer-email <practitioner>", prompt_zh="把申请材料发给代理师审核", prompt_en="Send the filing package to a practitioner for review", free=True))
|
|
338
|
+
from . import api as _api
|
|
339
|
+
|
|
340
|
+
if not _api.prosecution(case)["filings"]:
|
|
341
|
+
card["next"].append(_step(lang, "拿到申请号后记录申请日(自动算优先权和各项期限)", "Record the filing date once filed (priority and deadlines follow)", command=f"designaround filing record {case} --office {offices[0]} --filed <YYYY-MM-DD> --number <application-no>", prompt_zh="我已经提交了申请,申请日和申请号是:", prompt_en="I have filed; the filing date and number are: ", free=True))
|
|
342
|
+
card["managed"] = _managed(offices, redesign=False)
|
|
343
|
+
return done("submit", "proceed", "申请材料已备好,交代理师提交" if zh else "Filing package ready for the practitioner", "拿到申请号之后再上架或公开。" if zh else "Sell or publish only after you have the application number.")
|
|
344
|
+
|
|
345
|
+
if best or level in ("low", "not_seen"):
|
|
346
|
+
verdict_text = (f"改型方案 {best['id']} 达标,可以准备申请" if zh else f"Candidate {best['id']} qualifies; prepare the filing") if best else ("风险低,可以推进" if zh else "Low risk; you can proceed")
|
|
347
|
+
card["next"] += [
|
|
348
|
+
_step(lang, f"生成 {'/'.join(offices)} 申请材料包", f"Build the {'/'.join(offices)} filing package", command=f"designaround filing-pack {case} {target} --office {offices[0]}", prompt_zh=f"给 {target} 准备 {'、'.join(offices)} 的申请材料", prompt_en=f"Prepare the {', '.join(offices)} filing package for {target}", free=True),
|
|
349
|
+
_step(lang, "打样包(STEP/STL + 中文说明)", "Prototype pack (STEP/STL + factory notes)", command=f"designaround proto {case} {target}", prompt_zh=f"给 {target} 出打样包", prompt_en=f"Make the prototype pack for {target}", free=True),
|
|
350
|
+
]
|
|
351
|
+
card["managed"] = _managed(offices, redesign=False)
|
|
352
|
+
return done("file", "proceed", verdict_text, ("申请前别公开;代理师确认后提交。" if zh else "Do not publish before filing; a practitioner confirms and files."))
|
|
353
|
+
|
|
354
|
+
tried = [c for c in f["candidates"] if c["evaluated"]]
|
|
355
|
+
if level == "high" and len(tried) >= 6 and not qualified_candidates:
|
|
356
|
+
card["next"] += [
|
|
357
|
+
_step(lang, "换一个整体造型方向", "Try a different overall form", command=f'designaround design concepts {case} --count 4 --max-credits 680', prompt_zh="换几个整体造型完全不同的方向", prompt_en="Give me directions with a completely different overall form", credits=TYPICAL["concepts"]["credits"], minutes=TYPICAL["concepts"]["minutes"]),
|
|
358
|
+
_step(lang, "看托管报价(代理师参与)", "Managed service quote (with a practitioner)", command="designaround quote " + " ".join(f"--office {o}" for o in offices if o in ("US", "CN")) + " --managed", prompt_zh="我想了解托管服务的报价", prompt_en="Show me the managed service quote", free=True),
|
|
359
|
+
]
|
|
360
|
+
card["managed"] = _managed(offices, redesign=True)
|
|
361
|
+
return done("redesign", "abandon", "建议放弃或大改方向" if zh else "Consider dropping it or changing direction", (f"已评估 {len(tried)} 个改型方案,都没有降到合格线以下。" if zh else f"{len(tried)} assessed candidates; none came below the threshold."))
|
|
362
|
+
|
|
363
|
+
card["next"] += [
|
|
364
|
+
_step(lang, "自动改型(Agent 团队)", "Automatic design-around (agent team)", command=f"designaround around {case} --depth standard --max-credits {TYPICAL['around']['max_credits']}", prompt_zh="帮我做改型,保留主要功能", prompt_en="Design around it and keep the main function", credits=TYPICAL["around"]["credits"], max_credits=TYPICAL["around"]["max_credits"], minutes=TYPICAL["around"]["minutes"]),
|
|
365
|
+
_step(lang, "自己调参数后快速复检", "Adjust parameters and re-check quickly", command=f"designaround cad tweak {case} {original} --set <param>=<value>", prompt_zh="我想自己调几个参数", prompt_en="I want to adjust some parameters myself", free=True),
|
|
366
|
+
]
|
|
367
|
+
card["managed"] = _managed(offices, redesign=True)
|
|
368
|
+
return done("redesign", "redesign", "建议改了再做" if zh else "Redesign before you sell", (f"最高风险 {summary.get('max_risk', 0):.2f}({LEVEL[lang].get(level, level)}),高于合格线 {summary.get('threshold', 0.4):.2f}。" if zh else f"Highest risk {summary.get('max_risk', 0):.2f} ({LEVEL[lang].get(level, level)}), above the {summary.get('threshold', 0.4):.2f} threshold."))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def card_markdown(card: dict) -> str:
|
|
372
|
+
zh = card.get("lang") != "en"
|
|
373
|
+
lines = [f"**{card['headline']}** · {card['stage_label']}", "", card["summary"]]
|
|
374
|
+
if card["reasons"]:
|
|
375
|
+
lines += [""] + [f"- {r}" for r in card["reasons"]]
|
|
376
|
+
if card["next"]:
|
|
377
|
+
lines += ["", "**下一步**" if zh else "**Next**"]
|
|
378
|
+
for s in card["next"]:
|
|
379
|
+
cost = "免费" if zh and s.get("free") else ("free" if s.get("free") else (f"约 {s['credits']} 积分(${s['usd']})" if zh else f"~{s['credits']} credits (${s['usd']})") if s.get("credits") is not None else "")
|
|
380
|
+
when = (f",{s['minutes']} 分钟" if zh else f", {s['minutes']} min") if s.get("minutes") else ""
|
|
381
|
+
lines.append(f"- {s['label']}{':' if zh else ': '}{cost}{when}" + (f" — `{s['command']}`" if s.get("command") else ""))
|
|
382
|
+
if card.get("options"):
|
|
383
|
+
lines += ["", "| " + (" | ".join(["选择", "风险", "平台费", "申请费(估)", "时间"] if zh else ["Option", "Risk", "Platform", "Filing (est.)", "Time"])) + " |", "|---|---|---|---|---|"]
|
|
384
|
+
for o in card["options"]:
|
|
385
|
+
platform = f"{o['platform_credits']} 积分" if zh else f"{o['platform_credits']} credits"
|
|
386
|
+
lines.append(f"| {o['label']} | {o['risk']} | {platform} | ${o['filing_usd_estimate']:.0f} | {o.get('minutes') or '–'} |")
|
|
387
|
+
if card.get("managed"):
|
|
388
|
+
m = card["managed"]
|
|
389
|
+
lines += ["", (f"托管:平台费 ${m['platform_usd']} + 代理费约 ${m['practitioner_usd_est']}(直付代理师),官费另计" if zh else f"Managed: platform ${m['platform_usd']} + practitioner ~${m['practitioner_usd_est']} (paid to them directly), official fees extra")]
|
|
390
|
+
if card.get("blind_spots"):
|
|
391
|
+
lines += ["", ("盲区:" if zh else "Blind spots: ") + ";".join(card["blind_spots"][:3])]
|
|
392
|
+
lines += ["", f"_{card['wording']}_"]
|
|
393
|
+
return "\n".join(lines)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def card_path(case_id: str) -> Path:
|
|
397
|
+
return open_case(case_id).root / "card.json"
|