Files
TheTree_mock/docs/plans/original-completion-2026-10-02/execution/p22-03/build_backlog.py
T

112 lines
9.6 KiB
Python

"""Build a production planning draft from the accepted P22-01 usage inventory.
Usage-slot counts are not estimates of images, frames, clips, words, cost or effort.
"""
import argparse
import hashlib
import json
from pathlib import Path
INPUTS = ('facility_usage_slots', 'character_usage_slots', 'other_usage_slots', 'state_usage_groups')
COUNTS = (360, 176, 76, 2)
ROLE_PROFILES = {
'facility_art': ('施設ドット絵・背景制作担当候補', '施設・Lv・表裏・内外観ごとの完成見本、共有タイルと固有描画の適用表', 'タイル/画像数、解像度、共用範囲', '描画・差替・状態切替の組込み'),
'portrait': ('立ち絵制作担当候補', '人物別立ち絵と表情・衣装・状態・ルート差分の適用表', '人物ID、差分組合せ、画像数、解像度', '人物/イベントIDと差分選択の組込み'),
'motion': ('SDドット絵・アニメーション制作担当候補', '動作別モーションと対象人物・職種・状態の適用表', '対象人物、方向数、動作数、フレーム数、再生仕様', 'アニメーション・状態・移動との同期'),
'background': ('背景制作担当候補', '表裏の背景遠景と表示範囲', '画像数、解像度、分割/共用範囲', '背景描画・表示範囲・レイヤーの組込み'),
'ui': ('UI制作担当候補', '表裏スキン・操作/状態アイコン・各画面素材', '画面/状態一覧、画像数、サイズ、共用範囲', 'テーマ・操作・画面状態への組込み'),
'effect': ('エフェクト制作担当候補', '発生条件と完成エフェクトの適用表', '方式、画像/フレーム数、演出仕様', '発生条件・レイヤー切替との同期'),
'sound': ('作曲・編曲/効果音制作担当候補', '表裏アレンジ/環境音/操作音/必要SEと再生条件', '楽曲/クリップ数、長さ、ループ/切替/再生仕様', 'クロスフェード・再生条件・音量制御の組込み'),
'voice': ('音声制作・出演者候補(配役未定)', '人物別承認台本から収録、編集、台詞ID付き納品', '承認台詞ID、台詞数/文字数、演技差分、収録仕様', '台詞IDとの照合・再生・字幕同期'),
'event_art': ('イベントCG制作担当候補', 'イベント/ルートIDとCG・差分の適用表', '承認イベントID、画像/差分数、解像度', 'イベント/図鑑への登録と解放条件の組込み'),
'scope': ('内容監修・人物/状態設定担当候補', '未確定の人物・役割・エンド・状態と各制作物の対応表', '承認内容IDと対象範囲、各制作形式別数量', '正規ID・保存・条件・図鑑の契約整理'),
'script': ('シナリオ・台詞執筆担当候補', '章・人物・ルート・エンド・会話・図鑑の台本/内容IDと絵・声への対応表', '承認章/イベント/台詞/図鑑ID、文字数、選択肢/分岐数', '条件・イベント再生・保存・図鑑への組込み'),
}
def pin(path):
raw = path.read_bytes()
return {'path': str(path), 'bytes': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()}
def classify(kind, row):
ident = row.get('slot_id', row.get('group_id'))
if kind == 'facility_usage_slots' or ident.startswith('ancillary.'): return 'facility_art'
if ident == 'facility.visual_states': return 'facility_art'
if ident == 'unit.visual_states': return 'motion'
if ident.startswith(('characters.staff_role.', 'characters.guest_group.')) or ident in ('characters.mob_guests', 'characters.mob_staff'): return 'motion'
if ident.startswith('background.'): return 'background'
if ident.startswith('ui.'): return 'ui'
if ident.startswith('effect.'): return 'effect'
if ident == 'audio.character_voice' or row.get('kind') == 'voice': return 'voice'
if ident.startswith('audio.'): return 'sound'
if ident.startswith('content.') and ('event_cg' in ident or 'still' in ident): return 'event_art'
if row.get('kind', '').startswith('portrait_'): return 'portrait'
if row.get('kind', '').startswith('sd_'): return 'motion'
return 'scope'
def build(root, source):
pins = [pin(source / (n + '.json')) for n in INPUTS]
rows = []
for name, expected in zip(INPUTS, COUNTS):
original = json.loads((source / (name + '.json')).read_text())
assert len(original) == expected, name
for index, item in enumerate(original):
ident = item.get('slot_id', item.get('group_id'))
assert ident
rows.append({'source_id': ident, 'source_file': name + '.json', 'source_index': index,
'profile': classify(name, item),
'additional_profiles': ['scope'] if ident.startswith(('characters.staff_role.', 'characters.guest_group.')) or ident in ('characters.mob_guests', 'characters.mob_staff') else [],
'usage_condition': item['usage_condition'],
'source_anchors': item['source_anchors'],
'assigned_creator': None, 'assignment_approved': False,
'production_quantity': None, 'quantity_approved': False,
'estimated_content_hours': None, 'estimated_technical_hours': None,
'budget_amount': None, 'budget_approved': False,
'existing_representations': item.get('representations', []),
'reuse_approved': False, 'finished_quality_accepted': False,
'status': ['unassigned', 'unapproved', 'awaiting_quantity_and_reuse_decision'],
'decisions': ['D08', 'D09'] if classify(name, item) in ['script','scope','voice','event_art','portrait','motion'] else ['D09']})
assert len(rows) == 614 and len({x['source_id'] for x in rows}) == 614
baseline = root / 'docs/traceability/original/BASELINE.txt'
lines = baseline.read_text().splitlines()
for row in rows:
for anchor in row['source_anchors']:
f = root / anchor['path']
assert pin(f)['sha256'] == anchor['sha256']
assert f.read_text().splitlines()[anchor['line'] - 1] == anchor['text']
old_pins = json.loads((source / 'source_pins.json').read_text())
refreshed = []
for name, old in old_pins.items():
current = pin(root / old['path'])
refreshed.append({'relative_path': name, 'accepted_inventory_pin': old, 'current_pin': current,
'unchanged': old['sha256'] == current['sha256'],
'applicability': 'inventory reference only; current rendering/quality acceptance not inferred'})
profiles = {key: {'role_candidate': value[0], 'content_work': value[1], 'quantity_inputs': value[2],
'technical_work': value[3], 'estimate_method': '内容工数と技術工数を別に見積る。承認数量×制作形式別の見積単価、または担当別見積工数×提示単価に権利費を加える。単価/工数/金額は見積未取得のためnull。'}
for key, value in ROLE_PROFILES.items()}
# Script authoring is a cross-cutting dependency, not another image slot or delivery unit.
script_line = next(i for i,l in enumerate(lines, 1) if 'シナリオ・通常BGM組み込み' in l)
return {'schema_version': 1, 'plan_id': 'P22-03', 'formal_completion_allowed': False,
'scope': 'production-planning draft; 614 existing usage records, no new delivery units or atomic requirements',
'source_inputs': pins, 'baseline_pin': pin(baseline), 'current_reference_reaudit': refreshed,
'role_profiles': profiles, 'usage_to_production': rows,
'script_authoring': {'profile': 'script', 'source_anchor': {'path': str(baseline), 'line': script_line, 'text': lines[script_line-1]},
'linked_usage_ids': [r['source_id'] for r in rows if r['profile'] in ['event_art', 'voice', 'scope']],
'scope': 'chapter/person/route/end/dialogue/catalog scope requires D08; links do not enumerate all scripts',
'production_quantity': None, 'assigned_creator': None, 'estimated_content_hours': None,
'estimated_technical_hours': None, 'budget_amount': None,
'status': ['unassigned','unapproved','awaiting_D08_content_ids_and_quantities']},
'remaining_acceptance': ['必要枚数・差分/フレーム/音声/脚本量を承認内容ID・共用範囲と対応させる(利用枠数で代用不可)。',
'確定数量に対する担当候補・予算候補を内容制作/技術工数別に具体化し、全原work/acceptanceを独立受入する。'],
'not_authorized_by_this_draft': ['commissioning', 'payment', 'publishing', 'assignment approval', 'scope/quality reduction']}
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--root', type=Path, required=True)
parser.add_argument('--source', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
result = build(args.root.resolve(), args.source.resolve())
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({'mapped_usage_records': len(result['usage_to_production']),
'changed_references': [r['relative_path'] for r in result['current_reference_reaudit'] if not r['unchanged']],
'formal_completion_allowed': False}, ensure_ascii=False))