"""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))