# -*- coding: utf-8 -*-
# 動画「人型ロボットは人間より速く走ったのに、なぜ洗濯物はたためないのか(仮題)」の場面(謎解きの型の1本目)
PATTERN = 2 # 画面パターン2(大文字型・video_rules.md 4-4c)。2026-10-28 エイイチが決定(大文字型+動きあり)
MOVE = True # 画面型の動き(4-4d)を使う
# 画面の文字は「名前・数字・一言」だけ。中身は声と字幕に任せる
# 他社のロボット・大会の映像や写真・ロゴは使わない(許可がないため)。図はコードで描いた模式図だけ
# 謎解きの型(2026-10-28):
# - 出どころの札(左上): 大会や会社の発表(黒)/ニュース(青)/論文や本(灰の枠)/エイイチの記録と見立て(オレンジ)
# - エイイチ(ドット絵・声なし): ei_intro で真ん中に大きく登場 → ei_react・ei_desk*・mitate* で右下の小窓。ほかの場面には出さない
# ドット絵は 100 ドットの絵を等倍の整数倍で広げる(ぼかさない)。まばたきは閉じた目の絵を 0.15 秒だけ重ねる
# 強弱(4-4d): 強=叩きつけ・入れ替え(hook・hook_time・hook_answer・question・teaser・map2・c2_joke・solve・solve_fold・answer)
# 中=数え上げ(c1_sim・c3_figure)・エイイチの小窓(ぽん・まばたき)・机の上の映像
import os as _os, sys as _sys, json as _json, base64 as _b64
_here = _os.path.dirname(_os.path.abspath(__file__))
for _c in (_os.environ.get("P2M_BASE", ""), _os.path.join(_here, "..", "base"), _os.path.join(_here, "..", "_制作ベース")):
if _c and _os.path.exists(_os.path.join(_c, "scene_kit.py")):
_sys.path.insert(0, _c); break
from scene_kit import *
from scene_kit import _p2page
from motion_kit import use_motion
T1 = "1練習する場所"
T2 = "2相手の形"
T3 = "3お手本の数"
T4 = "4エイイチの見立て"
# --- 場面ごとの、文の始まりの秒(声の長さから)---
_idx = _json.load(open(_os.path.join(_here, 'tts/index.json'))) if _os.path.exists(_os.path.join(_here, 'tts/index.json')) else []
_LINES = {}
for _o in _idx:
_d = _LINES.setdefault(_o['scene'], [])
_t0 = (_d[-1][0] + _d[-1][1] + _d[-1][2]) if _d else 0.0
_d.append((_t0, _o['dur'], _o['pause']))
def at(scene, k=0, f=0.0):
"""その場面の k 番目の文の、読み上げの f 割の所(秒)"""
L = _LINES.get(scene)
if not L:
return 0.3
k = min(k, len(L) - 1)
return round(L[k][0] + L[k][1] * f, 2)
def scene_len(scene):
L = _LINES.get(scene)
return (L[-1][0] + L[-1][1] + L[-1][2]) if L else 4.0
# --- 出どころの札(左上)---
FUDA_CSS = """
.fuda{position:absolute;left:140px;top:96px;display:flex;align-items:center;gap:14px;font-size:30px;font-weight:700;color:#0D0D0D}
.fuda .k{padding:6px 18px;border-radius:999px;font-size:28px;font-weight:900}
.fuda .k.hap{background:#0D0D0D;color:#fff}
.fuda .k.news{background:#1A6C98;color:#fff}
.fuda .k.ken{border:3px solid #888888;color:#555}
.fuda .k.ei{background:#C4471F;color:#fff}
"""
FUDA_NAME = {"hap": "大会や会社の発表", "news": "ニュース", "ken": "論文や本", "ei": "エイイチの記録と見立て"}
def fuda(kind, who):
return f'
{FUDA_NAME[kind]}{who}
'
# --- エイイチ(ドット絵)---
def _uri(p):
return 'data:image/png;base64,' + _b64.b64encode(open(_os.path.join(_here, p), 'rb').read()).decode()
import ei_talk as _talk
EI = {k: (_talk.base_uri(k), _talk.base_uri(k, True)) for k in ('udegumi', 'unazuku', 'yubisasu', 'hirameku')}
# エイイチが話す文(narration.py の EI_LINES)→ (場面, 場面の中で何番目の文か, 文番号)
import importlib.util as _ilu
_sp = _ilu.spec_from_file_location('_narr_here', _os.path.join(_here, 'narration.py'))
_nm = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_nm)
_EIL = getattr(_nm, 'EI_LINES', [])
_EI_SPEAK = {}
_cnt = {}
for _o in _idx:
_k = _cnt.get(_o['scene'], 0); _cnt[_o['scene']] = _k + 1
if _o['i'] in _EIL:
_EI_SPEAK.setdefault(_o['scene'], []).append((_k, _o['i']))
EI_CSS = """
.eiw{position:absolute;right:10px;bottom:0px;width:420px;height:420px;overflow:hidden}
.eiw img{position:absolute;left:0;top:8px;width:420px;height:420px;image-rendering:pixelated}
.ein{position:absolute;right:40px;bottom:346px;width:300px;text-align:center;font-size:30px;font-weight:900;color:#0D0D0D}
.ein span{display:inline-block;background:#F5F5F0;border:3px solid #D6D6D1;border-radius:999px;padding:4px 20px}
"""
def _blinks(start, end):
t, out = start, []
while t < end - 0.4:
out.append(round(t, 2)); t += 3.3
return out
def ei_wipe(pose, scene, pop=None, name=None, poses=None):
"""右下の小窓。pop=秒 で、ぽんと出る。poses=[(秒, 絵)] で途中で絵を替える(指さしは大事な所だけ。2026-10-28 エイイチの指示)。
話している間は口と目を声に合わせて動かし、話していない間はまばたきだけする。
絵ごとにまとまり(div)を作り、替える時刻に前のまとまりを消して(out)、次を出す(fade)。絵の透明な所から前の絵が見えないように"""
seq_p = sorted(poses or [(0, pose)])
if seq_p[0][0] > 0:
seq_p = [(0, pose)] + seq_p
L = scene_len(scene) + 1
segs = [(t, seq_p[k + 1][0] if k + 1 < len(seq_p) else L, p) for k, (t, p) in enumerate(seq_p)]
spans, mouth, blinks = [], [], []
for k, i in _EI_SPEAK.get(scene, []):
t0 = at(scene, k, 0.0)
sq, end = _talk.mouth_seq(i, t0)
spans.append((t0, end)); mouth += sq
blinks += [(b, 0.13) for b in _talk.blink_times(t0, end)]
mouth.sort()
blinks += [(b, 0.15) for b in _blinks((pop or 0) + 1.3, scene_len(scene)) if not any(a0 - 0.2 <= b <= a1 + 0.2 for a0, a1 in spans)]
out = ''
for s0, s1, p in segs:
ev = []
lv = ([l for (t, l) in mouth if t <= s0] or [1])[-1]
if lv != 1 and p in _talk.MOUTH:
ev.append((s0, f'
'))
for t, l in mouth:
if s0 < t < s1 and p in _talk.MOUTH:
ev.append((t, f'
'))
e = _talk.eye_patches(p) if p != 'unazuku' else None
for b, d in blinks:
if e and s0 <= b and b + d < s1:
ev.append((b, f'
'))
ev.append((round(b + d, 2), f'
'))
ev.sort(key=lambda x: x[0])
inner = f'
' + ''.join(h for _, h in ev)
if s0 > 0:
inner = f'{inner}
'
if s1 < L:
inner = f'{inner}
'
out += inner
a = f' data-a="pop" data-t="{pop}" data-d="0.35"' if pop is not None else ''
nm = f'{name}
' if name else ''
return f'{nm}{out}
'
def add(page_html, *parts):
return page_html.replace('