Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,20 @@ jobs:
with:
python-version: '3.11'
cache: pip
- run: python -m pip install -e '.[test]' build
- run: python -m pytest -q tests
- run: python -m pip install -e '.[test]' build
- name: Download recommendation CLI
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p "$RUNNER_TEMP/deck/bin"
gh release download v0.0.3 --repo empty-sekai/ournotes-deck \
--pattern ournotes-deck-v0.0.3-x86_64-unknown-linux-musl.tar.gz \
--pattern SHA256SUMS --dir "$RUNNER_TEMP/deck"
cd "$RUNNER_TEMP/deck"
sha256sum --check --ignore-missing SHA256SUMS
tar -xzf ournotes-deck-v0.0.3-x86_64-unknown-linux-musl.tar.gz --strip-components=1 -C bin
echo "OURNOTES_DECK_BIN=$RUNNER_TEMP/deck/bin/ournotes-deck" >> "$GITHUB_ENV"
- run: python -m pytest -q -rs tests
- run: python scripts/validate_contract.py scan-request schemas/examples/scan-request.json
- run: python scripts/validate_contract.py scan-response schemas/examples/scan-response.json
- run: python -m build
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include docs/DECK_PIPELINE.md
include CHANGELOG.md
recursive-include tests/fixtures *.json
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ python boxvision.py scan --data DATA --threads 2 --output ./result screenshot-a.
可选的正式 `deck-ui` 入口将截图识别、显式人工补齐和 [OurNotes Deck](https://github.com/empty-sekai/ournotes-deck) 的真实搜索连在一起:

```powershell
python boxvision.py deck-ui --data DATA --deck-data deck-data.json --solver-bin ournotes-recommend.exe --workspace ./local/deck-ui --port 18790 --box-port 18793
python boxvision.py deck-ui --data DATA --deck-data deck-data.json --solver-bin ournotes-deck.exe --workspace ./local/deck-ui --port 18790 --box-port 18793
```

打开 `http://127.0.0.1:18790/`。需要与图库匹配的 DeckData 和支持 `ournotes-deck.recommendation-request/1` 的推荐程序;源码不附带游戏资源。未知字段不会默认填为等级/Rank 1,完整持有集合须由使用者声明。详细补齐契约、mock 复现、版本校验和目标语义见 [组卡输入管线](docs/DECK_PIPELINE.md)。
打开 `http://127.0.0.1:18790/`。需要与图库匹配的 DeckData 和 ournotes-deck v0.0.3(`recommend` 子命令、`ournotes-deck.search-request/1` 请求);源码不附带游戏资源。未知字段不会默认填为等级/Rank 1,完整持有集合须由使用者声明。详细补齐契约、mock 复现、版本校验和目标语义见 [组卡输入管线](docs/DECK_PIPELINE.md)。

## 数据和模型

Expand Down
8 changes: 3 additions & 5 deletions bdon_vision/deck/recommend.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@ def scene_matrix(deck_path):
shortest=min(d['charts'],key=lambda c:len(c['notes']['id']))
sid=shortest['scoreId']
base=next(r for r in rows(d,'MasterLiveMusic') if sid in [r.get('_easyID'),r.get('_normalID'),r.get('_hardID'),r.get('_expertID')])
seed={'atoms':[[1,1],[-1,1],[42,1]],'provenance':'Declared illustrative mock finite native root law; not the real TickCount/server distribution'}
specs=[]
def add(id_, execution, scenario=None, metric=None, expected='supported'):
req={'format':'ournotes-deck.recommendation-request/1','execution':execution,'metric':metric or {'kind':'score'},
req={'format':'ournotes-deck.search-request/1','execution':execution,'metric':metric or {'kind':'score'},
'constraints':{'noSnaps':False},'k':3,'strategy':{'kind':'exhaustive'},
'limits':{'timeLimitMs':30000,'maxCandidates':20000,'cacheEntries':2048}}
if scenario: req['scenario']=scenario
if execution['kind']=='live': req['seedLaw']=seed
specs.append({'id':id_,'request':req,'expected':expected})
add('songless-power',{'kind':'power'},metric={'kind':'power'})
for mode in ['free','mission','battle']:
Expand Down Expand Up @@ -114,7 +112,7 @@ def ticks(value):
entry['request']['strategy']={'kind':'candidate','powerSeeds':2,'proposals':600,'proposalSeed':20261001}
entry['request']['limits']={'timeLimitMs':10000,'maxCandidates':1000,'cacheEntries':1024}
return {'schema':'ournotes.integration-scene-matrix/1','owner':'BoxLens optional Deck recommendation workflow',
'deckDataSha256':digest(deck_path),'entries':specs,'liveIsSeparate':True,'capabilitySource':'ournotes-deck.recommendation-request/1','randomScope':'Explicit finite native root law and theoretical play; unknown real native TickCount distribution','unsupportedScope':'Battle/Arena require explicitly provided aggregate peer confirmations. Conditional mock Battle cases are separate; current JP has no Arena master rows'}
'deckDataSha256':digest(deck_path),'entries':specs,'liveIsSeparate':True,'capabilitySource':'ournotes-deck.search-request/1','randomScope':'Solver-reported probability law and theoretical-best play','unsupportedScope':'Battle/Arena require explicitly provided aggregate peer confirmations; available scenes depend on the bound regional master'}

def recommend(binary, deck_path, roster_path, matrix, out):
out=Path(out);out.mkdir(parents=True,exist_ok=True)
Expand All @@ -141,7 +139,7 @@ def recommend(binary, deck_path, roster_path, matrix, out):
if 'request' not in entry:
result.append({'id':id_,'state':'Unavailable','reason':entry.get('reason'),'complete':False});continue
request=entry['request'];reqfile=out/(id_+'-request.json');write_request(entry,reqfile)
command=[str(binary),'--data',str(frozen_deck),'--roster',str(frozen_roster),'--request',str(reqfile)]
command=[str(binary),'recommend','--data',str(frozen_deck),'--roster',str(frozen_roster),'--request',str(reqfile)]
t=time.perf_counter()
try:
p=subprocess.run(command,capture_output=True,encoding='utf-8',errors='replace',timeout=min(600,max(180,(request.get('limits',{}).get('timeLimitMs') or 165000)/1000+15)))
Expand Down
4 changes: 2 additions & 2 deletions bdon_vision/deck/web.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<section class="panel active" id="input"><div class="grid"><div class="card"><h2>截图输入与可选 mock 样本</h2><p>复用原始原生卡片 UI 生成器,含跨页重复、隐藏字段、降质与等级冲突。</p><div class="gallery" id="gallery"></div></div><div><div class="card"><h2>运行图片识别</h2><p>图片送到本机现有 BoxLens。识别器只接收图片、图库与模型。</p><input id="images" type="file" accept="image/*" multiple><label><input id="mock" type="checkbox"> 输入是声明的 mock 截图</label><div><button class="action" id="scan">识别所选截图</button></div><p><a id="boxlenslink" href="http://127.0.0.1:18781/" target="_blank">打开 BoxLens 原始校对界面 ↗</a></p><div class="note">截图只观察可见卡片;技能等级、角色 Rank、道具、VIP、回忆和完整持有集合需要单独提供。</div></div><div class="card"><h2>来源与版本</h2><div id="binding"></div><p><a href="/files/fixtures/fixture-manifest.json" target="_blank">生成器与截图真值清单 ↗</a></p></div></div></div></section>
<section class="panel" id="inventory"><div class="card"><div class="row"><h2>截图实际读到的字段</h2><span class="badge">观察集合 · 保留 unknown</span></div><div id="rawtable"></div><div class="note">同一卡片的等级出现两个值时,合并值保留为未知,冲突证据完整保留。隐藏字段与降质拒识也保留未知。</div><details><summary>查看原始 BoxLens JSON</summary><pre id="rawjson"></pre></details><a href="/files/scan/observations.json" target="_blank">逐图观察与模型来源 ↗</a></div></section>
<section class="panel" id="review"><div class="grid"><div class="card"><h2>已明确补齐的 Roster</h2><p>以下培养值来自识别或明确补充;技能与玩家状态的来源单独记录。</p><div id="rostertable"></div><div id="corrections"></div><details><summary>完整现有 Roster JSON</summary><pre id="rosterjson"></pre></details></div><div><div class="card"><h2>补充缺失信息</h2><p>演示真值与识别过程分开,点击后才用于补齐。真实账号请上传声明来源的人工补充文件。</p><button id="complete" class="action">采用声明的 mock 真值</button><input id="completionfile" type="file" accept="application/json"><button id="manual" class="action secondary">校验人工补充 JSON</button><p><a href="/files/fixtures/completion.json" target="_blank">查看补充契约样例 ↗</a></p><div id="player"></div><div class="note success">只有必要字段、持有集合和版本绑定全部校验通过,才导出 Roster 并进入搜索。</div></div></div></div></section>
<section class="panel" id="results"><div class="card"><div class="row"><div><h2>推荐队伍</h2><p id="scope">声明有限根种子分布下的 native Live;技能顺序由原生五槽洗牌决定。</p></div><span class="badge status" id="completionstatus"></span></div><div class="row"><div><div class="caption" id="metriclabel">目标指标</div><div class="resultmetric" id="metric"></div></div><div id="resultmeta"></div></div><div class="slots" id="slots"></div><div id="unselected" class="note success"></div><div class="note" id="modelscope">Complete 的最优性限定于请求中的卡池、约束、打法、有限 seedLaw 和模型范围。真实 TickCount / 服务器根种子总体分布未知。</div><details><summary>各根种子的技能顺序、分数与精确目标值</summary><pre id="outcomes"></pre></details></div><div class="card"><h2>场景矩阵与状态</h2><p>Live、Skip 与综合力分别验收。不可用、未支持、超时和候选搜索状态按真实返回展示。</p><select id="caseSelect"></select><button id="recommend" class="action">运行当前场景</button><input id="requestfile" type="file" accept="application/json"><button id="customrecommend" class="action secondary">运行明确提供的场景请求</button><p>活动指标、外部排名时间线与打法可由请求文件明确提供;搜索会校验实际 master 与支持范围。</p><div id="casetable"></div><details><summary>查看完整搜索结果与请求</summary><pre id="recommendjson"></pre></details></div></section>
<section class="panel" id="results"><div class="card"><div class="row"><div><h2>推荐队伍</h2><p id="scope">演出结果采用求解器返回的概率模型与请求中的打法。</p></div><span class="badge status" id="completionstatus"></span></div><div class="row"><div><div class="caption" id="metriclabel">目标指标</div><div class="resultmetric" id="metric"></div></div><div id="resultmeta"></div></div><div class="slots" id="slots"></div><div id="unselected" class="note success"></div><div class="note" id="modelscope">只有 Complete 且 optimality=proven 表示请求范围内的排名已经证明;概率模型与计算条件见完整结果。</div><details><summary>队伍分数与精确目标值</summary><pre id="outcomes"></pre></details></div><div class="card"><h2>场景矩阵与状态</h2><p>Live、Skip 与综合力分别验收。不可用、未支持、超时和候选搜索状态按真实返回展示。</p><select id="caseSelect"></select><button id="recommend" class="action">运行当前场景</button><input id="requestfile" type="file" accept="application/json"><button id="customrecommend" class="action secondary">运行明确提供的场景请求</button><p>活动指标、外部排名时间线与打法可由请求文件明确提供;搜索会校验实际 master 与支持范围。</p><div id="casetable"></div><details><summary>查看完整搜索结果与请求</summary><pre id="recommendjson"></pre></details></div></section>
<div id="message" aria-live="polite"></div><footer>图片、识别、校对和搜索在本机运行 · <a id="downloadRoster" href="/files/roster.json">下载 Roster</a> · <a id="downloadAdaptation" href="/files/adaptation.json">审查补齐与纠错来源</a> · <a href="/files/baseline-live-run.json">查看实际命令与耗时</a></footer></main>
<script>
const $=s=>document.querySelector(s),esc=s=>String(s??'—').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
Expand All @@ -24,7 +24,7 @@
function stamp(){return {input:inputVersion,request:requestVersion,scene:sceneVersion,session:sessionId};}
function current(t,scene=false){return t.input===inputVersion&&t.request===requestVersion&&t.session===sessionId&&(!scene||t.scene===sceneVersion);}
function fractionText(f,percent=false){try{let n=BigInt(f.numerator),d=BigInt(f.denominator);if(d===0n)return '—';if(percent)n*=100n;let neg=n<0n;n=neg?-n:n;let cents=(n*100n+d/2n)/d;return (neg?'-':'')+(cents/100n).toLocaleString('zh-CN')+'.'+String(cents%100n).padStart(2,'0');}catch{return '见精确值';}}
function metricSemantic(kind,execution,request){const live=execution?.kind==='live',skip=execution?.kind==='skip',play=request?.execution?.play?.kind==='stream'?'明确提供的判定序列':'理论最优打法';if(kind==='power')return {label:'综合力',unit:'综合力',scope:'按当前成员、培养状态、留影与场景加成计算综合力。',conditional:false};if(kind==='scoreAtLeast'&&skip)return {label:'跳过分数达标结果 · 分数 ≥ '+(request?.metric?.threshold??'请求阈值'),unit:'%',scope:'按指定跳过分数和阈值计算确定性达标结果(0% 或 100%)。',conditional:false};if(kind==='scoreAtLeast')return {label:'演出达标概率 · 分数 ≥ '+(request?.metric?.threshold??'请求阈值'),unit:'%',scope:'在声明的有限根种子分布与打法下,估计演出分数达到阈值的概率。',conditional:true};if(kind==='clientEventPoints')return {label:live?'期望活动收益':'跳过活动收益',unit:'活动 pt',scope:'根据明确提供的活动时间、计数与当前 master 计算客户端活动收益'+(live?',演出随机性限定于声明的有限 seedLaw。':'。'),conditional:live};if(kind==='conditionalClientEventItems')return {label:live?'期望目标道具收益':'跳过目标道具收益',unit:'个目标道具',scope:'以明确选择的奖励行、资源身份及活动计数为条件'+(live?',演出随机性限定于声明的有限 seedLaw。':'计算客户端道具数量。'),conditional:live};return {label:skip?'跳过分数':'期望演出分数',unit:'分',scope:skip?'按当前编成与指定谱面计算跳过分数。':'在声明的有限根种子分布与'+play+'下计算演出期望分数;技能顺序采用原生五槽洗牌。',conditional:live};}
function metricSemantic(kind,execution,request){const live=execution?.kind==='live',skip=execution?.kind==='skip',play=request?.execution?.play?.kind==='stream'?'明确提供的判定序列':'理论最优打法';if(kind==='power')return {label:'综合力',unit:'综合力',scope:'按当前成员、培养状态、留影与场景加成计算综合力。',conditional:false};if(kind==='scoreAtLeast'&&skip)return {label:'跳过分数达标结果 · 分数 ≥ '+(request?.metric?.threshold??'请求阈值'),unit:'%',scope:'按指定跳过分数和阈值计算确定性达标结果(0% 或 100%)。',conditional:false};if(kind==='scoreAtLeast')return {label:'演出达标概率 · 分数 ≥ '+(request?.metric?.threshold??'请求阈值'),unit:'%',scope:'按求解器的概率模型与声明打法计算演出分数达到阈值的概率。',conditional:true};if(kind==='clientEventPoints')return {label:live?'期望活动收益':'跳过活动收益',unit:'活动 pt',scope:'根据明确提供的活动时间、计数与当前 master 计算客户端活动收益'+(live?',演出随机性采用求解器返回的概率模型。':'。'),conditional:live};if(kind==='conditionalClientEventItems')return {label:live?'期望目标道具收益':'跳过目标道具收益',unit:'个目标道具',scope:'以明确选择的奖励行、资源身份及活动计数为条件'+(live?',演出随机性采用求解器返回的概率模型。':'计算客户端道具数量。'),conditional:live};return {label:skip?'跳过分数':'期望演出分数',unit:'分',scope:skip?'按当前编成与指定谱面计算跳过分数。':'按求解器的概率模型与'+play+'下计算演出期望分数;技能顺序采用原生五槽洗牌。',conditional:live};}
function errorText(c){const e=c?.raw?.error;if(typeof e==='string')return e;if(e)return [e.code||e.kind,e.message].filter(Boolean).join(':');return c?.reason||c?.state||'当前请求没有可展示队伍';}
function render(){$('#complete').disabled=!state.hasMockTruth;$('#binding').innerHTML='';if(state.boxLensUrl)$('#boxlenslink').href=state.boxLensUrl;let b=state.scan||state.inventory?.box,r=state.roster,m=state.manifest,a=state.adaptation;
if(b){$('#stats').innerHTML=[['观察身份',b.unique_count],['逐图观察',b.observations],['重复观察',b.duplicate_observations],['冲突卡片',b.cards.filter(x=>Object.keys(x.conflicts).length).length]].map(([label,n])=>'<div class="stat"><strong>'+n+'</strong><span>'+label+'</span></div>').join('');$('#rawtable').innerHTML=table(['身份','等级','卡阶','特训','证据'],b.cards.map(c=>[esc(c.kind+' #'+c.id),...['level','card_rank','awake_count'].map(f=>c.conflicts[f]?'<span class="warn">未知 · '+esc(Object.keys(c.conflicts[f]).join(' / '))+'</span>':esc(c[f].value??'unknown')),esc(c.sources.length+' 个来源')]));$('#rawjson').textContent=JSON.stringify(b,null,2);}else{$('#stats').textContent='等待新截图识别';$('#rawtable').textContent='';$('#rawjson').textContent='';}
Expand Down
Loading
Loading