In [109]:
!pip install numpy ipywidgets
Requirement already satisfied: numpy in /opt/anaconda3/lib/python3.12/site-packages (1.26.4)
Requirement already satisfied: ipywidgets in /opt/anaconda3/lib/python3.12/site-packages (7.8.1)
Requirement already satisfied: comm>=0.1.3 in /opt/anaconda3/lib/python3.12/site-packages (from ipywidgets) (0.2.1)
Requirement already satisfied: ipython-genutils~=0.2.0 in /opt/anaconda3/lib/python3.12/site-packages (from ipywidgets) (0.2.0)
Requirement already satisfied: traitlets>=4.3.1 in /opt/anaconda3/lib/python3.12/site-packages (from ipywidgets) (5.14.3)
Requirement already satisfied: widgetsnbextension~=3.6.6 in /opt/anaconda3/lib/python3.12/site-packages (from ipywidgets) (3.6.6)
Requirement already satisfied: ipython>=4.0.0 in /opt/anaconda3/lib/python3.12/site-packages (from ipywidgets) (8.27.0)
Requirement already satisfied: jupyterlab-widgets<3,>=1.0.0 in /opt/anaconda3/lib/python3.12/site-packages (from ipywidgets) (1.0.0)
Requirement already satisfied: decorator in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (5.1.1)
Requirement already satisfied: jedi>=0.16 in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (0.19.1)
Requirement already satisfied: matplotlib-inline in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (0.1.6)
Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.41 in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (3.0.43)
Requirement already satisfied: pygments>=2.4.0 in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (2.15.1)
Requirement already satisfied: stack-data in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (0.2.0)
Requirement already satisfied: pexpect>4.3 in /opt/anaconda3/lib/python3.12/site-packages (from ipython>=4.0.0->ipywidgets) (4.8.0)
Requirement already satisfied: notebook>=4.4.1 in /opt/anaconda3/lib/python3.12/site-packages (from widgetsnbextension~=3.6.6->ipywidgets) (7.2.2)
Requirement already satisfied: parso<0.9.0,>=0.8.3 in /opt/anaconda3/lib/python3.12/site-packages (from jedi>=0.16->ipython>=4.0.0->ipywidgets) (0.8.3)
Requirement already satisfied: jupyter-server<3,>=2.4.0 in /opt/anaconda3/lib/python3.12/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.14.1)
Requirement already satisfied: jupyterlab-server<3,>=2.27.1 in /opt/anaconda3/lib/python3.12/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.27.3)
Requirement already satisfied: jupyterlab<4.3,>=4.2.0 in /opt/anaconda3/lib/python3.12/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (4.2.5)
Requirement already satisfied: notebook-shim<0.3,>=0.2 in /opt/anaconda3/lib/python3.12/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.2.3)
Requirement already satisfied: tornado>=6.2.0 in /opt/anaconda3/lib/python3.12/site-packages (from notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (6.4.1)
Requirement already satisfied: ptyprocess>=0.5 in /opt/anaconda3/lib/python3.12/site-packages (from pexpect>4.3->ipython>=4.0.0->ipywidgets) (0.7.0)
Requirement already satisfied: wcwidth in /opt/anaconda3/lib/python3.12/site-packages (from prompt-toolkit<3.1.0,>=3.0.41->ipython>=4.0.0->ipywidgets) (0.2.5)
Requirement already satisfied: executing in /opt/anaconda3/lib/python3.12/site-packages (from stack-data->ipython>=4.0.0->ipywidgets) (0.8.3)
Requirement already satisfied: asttokens in /opt/anaconda3/lib/python3.12/site-packages (from stack-data->ipython>=4.0.0->ipywidgets) (2.0.5)
Requirement already satisfied: pure-eval in /opt/anaconda3/lib/python3.12/site-packages (from stack-data->ipython>=4.0.0->ipywidgets) (0.2.2)
Requirement already satisfied: anyio>=3.1.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (4.2.0)
Requirement already satisfied: argon2-cffi>=21.1 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (21.3.0)
Requirement already satisfied: jinja2>=3.0.3 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (3.1.4)
Requirement already satisfied: jupyter-client>=7.4.4 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (8.6.0)
Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (5.7.2)
Requirement already satisfied: jupyter-events>=0.9.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.10.0)
Requirement already satisfied: jupyter-server-terminals>=0.4.4 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.4.4)
Requirement already satisfied: nbconvert>=6.4.4 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (7.16.4)
Requirement already satisfied: nbformat>=5.3.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (5.10.4)
Requirement already satisfied: overrides>=5.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (7.4.0)
Requirement already satisfied: packaging>=22.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (24.1)
Requirement already satisfied: prometheus-client>=0.9 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.14.1)
Requirement already satisfied: pyzmq>=24 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (25.1.2)
Requirement already satisfied: send2trash>=1.8.2 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.8.2)
Requirement already satisfied: terminado>=0.8.3 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.17.1)
Requirement already satisfied: websocket-client>=1.7 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.8.0)
Requirement already satisfied: async-lru>=1.0.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.0.4)
Requirement already satisfied: httpx>=0.25.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.27.0)
Requirement already satisfied: ipykernel>=6.5.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (6.28.0)
Requirement already satisfied: jupyter-lsp>=2.0.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.2.0)
Requirement already satisfied: setuptools>=40.1.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (75.1.0)
Requirement already satisfied: babel>=2.10 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.11.0)
Requirement already satisfied: json5>=0.9.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.9.6)
Requirement already satisfied: jsonschema>=4.18.0 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (4.23.0)
Requirement already satisfied: requests>=2.31 in /opt/anaconda3/lib/python3.12/site-packages (from jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.32.3)
Requirement already satisfied: six in /opt/anaconda3/lib/python3.12/site-packages (from asttokens->stack-data->ipython>=4.0.0->ipywidgets) (1.16.0)
Requirement already satisfied: idna>=2.8 in /opt/anaconda3/lib/python3.12/site-packages (from anyio>=3.1.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (3.7)
Requirement already satisfied: sniffio>=1.1 in /opt/anaconda3/lib/python3.12/site-packages (from anyio>=3.1.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.3.0)
Requirement already satisfied: argon2-cffi-bindings in /opt/anaconda3/lib/python3.12/site-packages (from argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (21.2.0)
Requirement already satisfied: pytz>=2015.7 in /opt/anaconda3/lib/python3.12/site-packages (from babel>=2.10->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2024.1)
Requirement already satisfied: certifi in /opt/anaconda3/lib/python3.12/site-packages (from httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2025.4.26)
Requirement already satisfied: httpcore==1.* in /opt/anaconda3/lib/python3.12/site-packages (from httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.0.2)
Requirement already satisfied: h11<0.15,>=0.13 in /opt/anaconda3/lib/python3.12/site-packages (from httpcore==1.*->httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.14.0)
Requirement already satisfied: appnope in /opt/anaconda3/lib/python3.12/site-packages (from ipykernel>=6.5.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.1.3)
Requirement already satisfied: debugpy>=1.6.5 in /opt/anaconda3/lib/python3.12/site-packages (from ipykernel>=6.5.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.6.7)
Requirement already satisfied: nest-asyncio in /opt/anaconda3/lib/python3.12/site-packages (from ipykernel>=6.5.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.6.0)
Requirement already satisfied: psutil in /opt/anaconda3/lib/python3.12/site-packages (from ipykernel>=6.5.0->jupyterlab<4.3,>=4.2.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (5.9.0)
Requirement already satisfied: MarkupSafe>=2.0 in /opt/anaconda3/lib/python3.12/site-packages (from jinja2>=3.0.3->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.1.3)
Requirement already satisfied: attrs>=22.2.0 in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (23.1.0)
Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2023.7.1)
Requirement already satisfied: referencing>=0.28.4 in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.30.2)
Requirement already satisfied: rpds-py>=0.7.1 in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.10.6)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-client>=7.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.9.0.post0)
Requirement already satisfied: platformdirs>=2.5 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-core!=5.0.*,>=4.12->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (3.10.0)
Requirement already satisfied: python-json-logger>=2.0.4 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.0.7)
Requirement already satisfied: pyyaml>=5.3 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (6.0.1)
Requirement already satisfied: rfc3339-validator in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.1.4)
Requirement already satisfied: rfc3986-validator>=0.1.1 in /opt/anaconda3/lib/python3.12/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.1.1)
Requirement already satisfied: beautifulsoup4 in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (4.12.3)
Requirement already satisfied: bleach!=5.0.0 in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (4.1.0)
Requirement already satisfied: defusedxml in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.7.1)
Requirement already satisfied: jupyterlab-pygments in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.1.2)
Requirement already satisfied: mistune<4,>=2.0.3 in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.0.4)
Requirement already satisfied: nbclient>=0.5.0 in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.8.0)
Requirement already satisfied: pandocfilters>=1.4.1 in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.5.0)
Requirement already satisfied: tinycss2 in /opt/anaconda3/lib/python3.12/site-packages (from nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.2.1)
Requirement already satisfied: fastjsonschema>=2.15 in /opt/anaconda3/lib/python3.12/site-packages (from nbformat>=5.3.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.16.2)
Requirement already satisfied: charset-normalizer<4,>=2 in /opt/anaconda3/lib/python3.12/site-packages (from requests>=2.31->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (3.3.2)
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/anaconda3/lib/python3.12/site-packages (from requests>=2.31->jupyterlab-server<3,>=2.27.1->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.2.3)
Requirement already satisfied: webencodings in /opt/anaconda3/lib/python3.12/site-packages (from bleach!=5.0.0->nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (0.5.1)
Requirement already satisfied: fqdn in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.5.1)
Requirement already satisfied: isoduration in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (20.11.0)
Requirement already satisfied: jsonpointer>1.13 in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.1)
Requirement already satisfied: uri-template in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.3.0)
Requirement already satisfied: webcolors>=24.6.0 in /opt/anaconda3/lib/python3.12/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (25.10.0)
Requirement already satisfied: cffi>=1.0.1 in /opt/anaconda3/lib/python3.12/site-packages (from argon2-cffi-bindings->argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.17.1)
Requirement already satisfied: soupsieve>1.2 in /opt/anaconda3/lib/python3.12/site-packages (from beautifulsoup4->nbconvert>=6.4.4->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.5)
Requirement already satisfied: pycparser in /opt/anaconda3/lib/python3.12/site-packages (from cffi>=1.0.1->argon2-cffi-bindings->argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (2.21)
Requirement already satisfied: arrow>=0.15.0 in /opt/anaconda3/lib/python3.12/site-packages (from isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook>=4.4.1->widgetsnbextension~=3.6.6->ipywidgets) (1.2.3)
In [110]:
!pip install chardet --quiet
In [111]:
# Cell 1
import json, math
from pathlib import Path
from datetime import datetime
import numpy as np
from IPython.display import display, Markdown, HTML, clear_output
import ipywidgets as widgets

OUT_DIR = Path("demo_outputs")
OUT_DIR.mkdir(exist_ok=True)
def save_json(obj, name):
    p = OUT_DIR / name
    p.write_text(json.dumps(obj, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"Saved {p.resolve()}")
def show(obj, title=None):
    if title: display(Markdown(f"### {title}"))
    display(HTML(f"<pre>{json.dumps(obj, ensure_ascii=False, indent=2)}</pre>"))
In [112]:
# Cell 2
questionnaire = {
  "title":"KOC 风格自评问卷(快速版,6题)",
  "questions":[
    {"id":"q1","text":"当你上镜讲内容时,你更偏向哪种方式?","options":[
      {"id":"q1a","text":"讲故事/生活化","scores":{"Humor":10,"Emotionality":70,"Rationality":20,"Pace":40,"Spontaneity":50}},
      {"id":"q1b","text":"讲数据/功能","scores":{"Humor":5,"Emotionality":20,"Rationality":80,"Pace":40,"Spontaneity":30}},
      {"id":"q1c","text":"幽默/夸张","scores":{"Humor":90,"Emotionality":50,"Rationality":10,"Pace":80,"Spontaneity":80}},
      {"id":"q1d","text":"平和稳重","scores":{"Humor":5,"Emotionality":50,"Rationality":50,"Pace":20,"Spontaneity":20}}
    ]},
    {"id":"q2","text":"面对镜头,你的舒适度?","options":[
      {"id":"q2a","text":"非常自在","scores":{"Humor":40,"Emotionality":40,"Rationality":30,"Pace":60,"Spontaneity":90}},
      {"id":"q2b","text":"略微紧张","scores":{"Humor":20,"Emotionality":40,"Rationality":50,"Pace":40,"Spontaneity":50}},
      {"id":"q2c","text":"倾向背台词","scores":{"Humor":10,"Emotionality":30,"Rationality":70,"Pace":30,"Spontaneity":20}},
      {"id":"q2d","text":"不太习惯","scores":{"Humor":5,"Emotionality":20,"Rationality":60,"Pace":20,"Spontaneity":10}}
    ]},
    {"id":"q3","text":"视频节奏偏好?","options":[
      {"id":"q3a","text":"快节奏","scores":{"Humor":60,"Emotionality":50,"Rationality":20,"Pace":90,"Spontaneity":70}},
      {"id":"q3b","text":"中速","scores":{"Humor":30,"Emotionality":50,"Rationality":50,"Pace":50,"Spontaneity":50}},
      {"id":"q3c","text":"慢讲解","scores":{"Humor":10,"Emotionality":40,"Rationality":80,"Pace":20,"Spontaneity":30}},
      {"id":"q3d","text":"场景铺陈","scores":{"Humor":20,"Emotionality":80,"Rationality":30,"Pace":30,"Spontaneity":40}}
    ]},
    {"id":"q4","text":"希望给人印象?","options":[
      {"id":"q4a","text":"有趣/亲切","scores":{"Humor":80,"Emotionality":60,"Rationality":20,"Pace":60,"Spontaneity":60}},
      {"id":"q4b","text":"专业/靠谱","scores":{"Humor":10,"Emotionality":30,"Rationality":90,"Pace":40,"Spontaneity":30}},
      {"id":"q4c","text":"温暖/可信","scores":{"Humor":20,"Emotionality":80,"Rationality":40,"Pace":30,"Spontaneity":40}},
      {"id":"q4d","text":"真诚/接地气","scores":{"Humor":20,"Emotionality":70,"Rationality":40,"Pace":30,"Spontaneity":50}}
    ]},
    {"id":"q5","text":"能接受的改动幅度?","options":[
      {"id":"q5a","text":"大幅尝试","scores":{"Humor":40,"Emotionality":40,"Rationality":40,"Pace":40,"Spontaneity":80}},
      {"id":"q5b","text":"中度尝试","scores":{"Humor":30,"Emotionality":30,"Rationality":50,"Pace":50,"Spontaneity":60}},
      {"id":"q5c","text":"轻微调整","scores":{"Humor":10,"Emotionality":20,"Rationality":60,"Pace":30,"Spontaneity":30}},
      {"id":"q5d","text":"不愿改变","scores":{"Humor":5,"Emotionality":10,"Rationality":70,"Pace":20,"Spontaneity":10}}
    ]},
    {"id":"q6","text":"形象训练投入?","options":[
      {"id":"q6a","text":"高","scores":{"Humor":40,"Emotionality":40,"Rationality":30,"Pace":60,"Spontaneity":80}},
      {"id":"q6b","text":"中","scores":{"Humor":30,"Emotionality":30,"Rationality":50,"Pace":40,"Spontaneity":50}},
      {"id":"q6c","text":"低","scores":{"Humor":15,"Emotionality":20,"Rationality":60,"Pace":30,"Spontaneity":30}},
      {"id":"q6d","text":"极低","scores":{"Humor":5,"Emotionality":10,"Rationality":70,"Pace":20,"Spontaneity":10}}
    ]}
  ],
  "post_processing":{"method":"sum_then_normalize","weight_questionnaire":0.4}
}

save_json(questionnaire, "questionnaire.json")
print("Questionnaire saved.")
Saved /Users/vallenliu/demo_outputs/questionnaire.json
Questionnaire saved.
In [113]:
# Cell 3
from datetime import datetime, timezone

def compute_style_vector_from_answers(answers, questionnaire):
    dims = {"Humor":0,"Emotionality":0,"Rationality":0,"Pace":0,"Spontaneity":0}
    for q, ans in zip(questionnaire["questions"], answers):
        opt = next(o for o in q["options"] if o["id"]==ans)
        for k,v in opt["scores"].items():
            dims[k] += v
    max_per = len(questionnaire["questions"]) * 100
    style = {k: round(v / max_per * 100,1) for k,v in dims.items()}
    # naive confidence: higher mean -> higher confidence (placeholder)
    conf = round(sum(style.values())/(len(style)*100),2)
    return style, conf

# Demo answers (pick first option for simplicity)
demo_answers = [q["options"][0]["id"] for q in questionnaire["questions"]]
style_vec, style_conf = compute_style_vector_from_answers(demo_answers, questionnaire)
koc_profile = {
    "id":"koc_demo_001",
    "name":"Demo KOC",
    "style_vector": {k: {"val":v, "conf":style_conf} for k,v in style_vec.items()},
    "capability":{"shoot":60,"edit":50,"improv":40},
    "preferences":{"categories":["护肤","美妆"], "price_range":"low"},
    "consent_flags":{"orders":False,"followers":False},
    "created_at": datetime.now(timezone.utc).isoformat()
}
save_json(koc_profile, "koc_profile.json")
show(koc_profile, "KOC Profile (module A)")
Saved /Users/vallenliu/demo_outputs/koc_profile.json

KOC Profile (module A)¶

{
  "id": "koc_demo_001",
  "name": "Demo KOC",
  "style_vector": {
    "Humor": {
      "val": 45.0,
      "conf": 0.5
    },
    "Emotionality": {
      "val": 50.0,
      "conf": 0.5
    },
    "Rationality": {
      "val": 26.7,
      "conf": 0.5
    },
    "Pace": {
      "val": 58.3,
      "conf": 0.5
    },
    "Spontaneity": {
      "val": 71.7,
      "conf": 0.5
    }
  },
  "capability": {
    "shoot": 60,
    "edit": 50,
    "improv": 40
  },
  "preferences": {
    "categories": [
      "护肤",
      "美妆"
    ],
    "price_range": "low"
  },
  "consent_flags": {
    "orders": false,
    "followers": false
  },
  "created_at": "2026-04-25T02:35:47.882233+00:00"
}
In [114]:
# Cell 4
def generate_audience_from_history_or_coldstart(koc_profile, has_history=False):
    if has_history:
        # placeholder: analyze history -> produce segments
        segments = [{"id":"s1","label":"女性25-34敏感肌","age_range":[25,34],"gender":"F","interest_tags":["护肤"],"conf":0.85,"sample_size":1200}]
    else:
        # cold start: use declared prefs + platform heuristics
        segments = [{"id":"s_demo","label":"默认年轻女性","age_range":[18,34],"gender":"F","interest_tags":koc_profile["preferences"]["categories"],"conf":0.45,"sample_size":80}]
    return {"koc_id":koc_profile["id"], "segments":segments, "suggested_test_plan":"Run 2 variants in private group, 500 impressions each"}

aud_profile = generate_audience_from_history_or_coldstart(koc_profile, has_history=False)
save_json(aud_profile, "audience_profile.json")
show(aud_profile, "Audience Profile (module B)")
Saved /Users/vallenliu/demo_outputs/audience_profile.json

Audience Profile (module B)¶

{
  "koc_id": "koc_demo_001",
  "segments": [
    {
      "id": "s_demo",
      "label": "默认年轻女性",
      "age_range": [
        18,
        34
      ],
      "gender": "F",
      "interest_tags": [
        "护肤",
        "美妆"
      ],
      "conf": 0.45,
      "sample_size": 80
    }
  ],
  "suggested_test_plan": "Run 2 variants in private group, 500 impressions each"
}
In [115]:
# Cell 5
def norm_margin(margin_pct): return min(1.0, margin_pct/0.4)
def norm_inventory(inv, thr=1000): return min(1.0, inv/thr)
def norm_lead(days, maxd=14): return max(0.0, (maxd - min(days, maxd))/maxd)
def norm_sample(can_sample, sample_days=None, maxd=7):
    if can_sample:
        if sample_days is None: return 1.0
        return max(0.0, (maxd - min(sample_days, maxd))/maxd)
    return 0.0

def compute_supply_score(candidate, weights=None):
    if weights is None:
        weights = {"w1":0.30,"w2":0.25,"w3":0.15,"w4":0.15,"w5":0.15}
    ss = candidate.get("supply_signals",{})
    T = norm_sample(ss.get("can_sample", False), ss.get("sample_time_days", None))
    M = norm_margin(candidate.get("margin_pct",0.2))
    I = norm_inventory(ss.get("inventory",0))
    L = norm_lead(ss.get("lead_time_days",14))
    B = candidate.get("brand_score",50)/100.0
    num = weights["w1"]*T + weights["w2"]*M + weights["w3"]*I + weights["w4"]*L + weights["w5"]*B
    return round(100 * num / sum(weights.values()),1)

candidate = {
  "id":"sku123","name":"护肤X霜(敏感型)","brand":"小众牌A","price":79.0,"source_tag":"护肤",
  "evidence":[{"type":"ecom_trend","text":"近30天搜索↑45%","url":""}],
  "supply_signals":{"can_sample":True,"sample_time_days":5,"inventory":800,"lead_time_days":3},
  "margin_pct":0.35,"brand_score":78
}
supply_score = compute_supply_score(candidate)
save_json(candidate, "product_candidate.json")
print("supply_score:", supply_score)
show({"supply_score":supply_score}, "Supply Score (module C)")
Saved /Users/vallenliu/demo_outputs/product_candidate.json
supply_score: 65.9

Supply Score (module C)¶

{
  "supply_score": 65.9
}
In [116]:
# Cell 6
def simple_audience_match(candidate, audience_profile):
    seg = audience_profile["segments"][0]
    score = 0.0
    if candidate.get("source_tag") in seg.get("interest_tags",[]):
        score += 0.6
    pref_price = "low"  # simplified
    price = candidate.get("price", 0)
    if pref_price=="low" and price<100: score += 0.4
    return round(min(1.0,score)*100,1)

def rank_score(candidate, audience_profile, trend_score=50, alpha=0.5, beta=0.35, gamma=0.15):
    a = simple_audience_match(candidate, audience_profile)
    s = compute_supply_score(candidate)
    return round(alpha*a + beta*s + gamma*trend_score,1)

aud_match = simple_audience_match(candidate, aud_profile)
rank = rank_score(candidate, aud_profile)
candidates_list = [candidate]
for c in candidates_list:
    c["supply_score"] = compute_supply_score(c)
    c["audience_match_score"] = simple_audience_match(c, aud_profile)
    c["rank_score"] = rank_score(c, aud_profile)
save_json({"candidates":candidates_list}, "candidates_list.json")
show(candidates_list, "Candidates (module C output)")
Saved /Users/vallenliu/demo_outputs/candidates_list.json

Candidates (module C output)¶

[
  {
    "id": "sku123",
    "name": "护肤X霜(敏感型)",
    "brand": "小众牌A",
    "price": 79.0,
    "source_tag": "护肤",
    "evidence": [
      {
        "type": "ecom_trend",
        "text": "近30天搜索↑45%",
        "url": ""
      }
    ],
    "supply_signals": {
      "can_sample": true,
      "sample_time_days": 5,
      "inventory": 800,
      "lead_time_days": 3
    },
    "margin_pct": 0.35,
    "brand_score": 78,
    "supply_score": 65.9,
    "audience_match_score": 100.0,
    "rank_score": 80.6
  }
]
In [117]:
# Cell 7
def generate_script_variants(koc_profile, candidate, num=3):
    hooks = ["最近用了这个,效果挺让我惊喜。","性价比超高,值得一试。","真心推荐,尤其适合你们。"]
    tones = ["温和可信","中性推荐","情绪化种草"]
    variants=[]
    for i in range(num):
        variants.append({
            "id":f"scr_{candidate['id']}_{i}",
            "hook":hooks[i%len(hooks)],
            "body":f"产品:{candidate['name']},价格{candidate['price']}元,卖点:{candidate['evidence'][0]['text']}",
            "cta":"点击链接了解/私信拿优惠",
            "style_label":tones[i%len(tones)],
            "target_segment":aud_profile["segments"][0]["id"],
            "evidence":candidate["evidence"]
        })
    return variants

scripts = generate_script_variants(koc_profile, candidate, num=3)
save_json({"scripts":scripts}, "scripts.json")
show(scripts, "Scripts (module D)")
Saved /Users/vallenliu/demo_outputs/scripts.json

Scripts (module D)¶

[
  {
    "id": "scr_sku123_0",
    "hook": "最近用了这个,效果挺让我惊喜。",
    "body": "产品:护肤X霜(敏感型),价格79.0元,卖点:近30天搜索↑45%",
    "cta": "点击链接了解/私信拿优惠",
    "style_label": "温和可信",
    "target_segment": "s_demo",
    "evidence": [
      {
        "type": "ecom_trend",
        "text": "近30天搜索↑45%",
        "url": ""
      }
    ]
  },
  {
    "id": "scr_sku123_1",
    "hook": "性价比超高,值得一试。",
    "body": "产品:护肤X霜(敏感型),价格79.0元,卖点:近30天搜索↑45%",
    "cta": "点击链接了解/私信拿优惠",
    "style_label": "中性推荐",
    "target_segment": "s_demo",
    "evidence": [
      {
        "type": "ecom_trend",
        "text": "近30天搜索↑45%",
        "url": ""
      }
    ]
  },
  {
    "id": "scr_sku123_2",
    "hook": "真心推荐,尤其适合你们。",
    "body": "产品:护肤X霜(敏感型),价格79.0元,卖点:近30天搜索↑45%",
    "cta": "点击链接了解/私信拿优惠",
    "style_label": "情绪化种草",
    "target_segment": "s_demo",
    "evidence": [
      {
        "type": "ecom_trend",
        "text": "近30天搜索↑45%",
        "url": ""
      }
    ]
  }
]
In [118]:
# Cell 8
def gen_storyboard(script_text, style_vector, length_sec=60):
    seg = length_sec // 3
    sb=[]
    for i in range(3):
        sb.append({"start":i*seg,"end":(i+1)*seg if i<2 else length_sec,
                   "action":["钩子","核心卖点","结尾CTA"][i],
                   "camera":["近景","中景","特写"][i],
                   "dialogue":f"第{i+1}段示例台词","cues":["表情","展示细节","强调CTA"][i]})
    return sb

def gen_subtitles(storyboard):
    srt=[]
    for i,s in enumerate(storyboard,1):
        srt.append({"index":i,"start":f"00:00:{s['start']:02d}","end":f"00:00:{s['end']:02d}","text":s["dialogue"]})
    return srt

def gen_cut_list(storyboard):
    return [{"start":s["start"],"end":s["end"],"reason":s["action"],"replace_with":"b-roll_closeup"} for s in storyboard]

def gen_makeup_plan(candidate, koc_profile):
    if "护肤" in candidate.get("source_tag",""):
        return {"level":"推荐","steps":["清洁","保湿","轻薄粉底","自然腮红"],"color_palette":["#F2C1C1","#FFF6EA"],"confidence":0.78}
    return {"level":"简易","steps":["自然妆"],"confidence":0.5}

storyboard = gen_storyboard("示例脚本", koc_profile["style_vector"], length_sec=60)
subtitles = gen_subtitles(storyboard)
cut_list = gen_cut_list(storyboard)
makeup_plan = gen_makeup_plan(candidate, koc_profile)

material = {
    "storyboard":storyboard,
    "subtitles":subtitles,
    "cut_list":cut_list,
    "makeup_plan":makeup_plan,
    "covers":[{"url":"cover1.png","prompt":"面部特写+大字钩子","ctr_score":0.12,"uncertainty":0.03}],
    "variants":[{"id":"v1","desc":"快节奏钩子"},{"id":"v2","desc":"慢节奏讲解"}]
}
save_json(material, "material_package.json")
show(material, "Material Package (module E)")
Saved /Users/vallenliu/demo_outputs/material_package.json

Material Package (module E)¶

{
  "storyboard": [
    {
      "start": 0,
      "end": 20,
      "action": "钩子",
      "camera": "近景",
      "dialogue": "第1段示例台词",
      "cues": "表情"
    },
    {
      "start": 20,
      "end": 40,
      "action": "核心卖点",
      "camera": "中景",
      "dialogue": "第2段示例台词",
      "cues": "展示细节"
    },
    {
      "start": 40,
      "end": 60,
      "action": "结尾CTA",
      "camera": "特写",
      "dialogue": "第3段示例台词",
      "cues": "强调CTA"
    }
  ],
  "subtitles": [
    {
      "index": 1,
      "start": "00:00:00",
      "end": "00:00:20",
      "text": "第1段示例台词"
    },
    {
      "index": 2,
      "start": "00:00:20",
      "end": "00:00:40",
      "text": "第2段示例台词"
    },
    {
      "index": 3,
      "start": "00:00:40",
      "end": "00:00:60",
      "text": "第3段示例台词"
    }
  ],
  "cut_list": [
    {
      "start": 0,
      "end": 20,
      "reason": "钩子",
      "replace_with": "b-roll_closeup"
    },
    {
      "start": 20,
      "end": 40,
      "reason": "核心卖点",
      "replace_with": "b-roll_closeup"
    },
    {
      "start": 40,
      "end": 60,
      "reason": "结尾CTA",
      "replace_with": "b-roll_closeup"
    }
  ],
  "makeup_plan": {
    "level": "推荐",
    "steps": [
      "清洁",
      "保湿",
      "轻薄粉底",
      "自然腮红"
    ],
    "color_palette": [
      "#F2C1C1",
      "#FFF6EA"
    ],
    "confidence": 0.78
  },
  "covers": [
    {
      "url": "cover1.png",
      "prompt": "面部特写+大字钩子",
      "ctr_score": 0.12,
      "uncertainty": 0.03
    }
  ],
  "variants": [
    {
      "id": "v1",
      "desc": "快节奏钩子"
    },
    {
      "id": "v2",
      "desc": "慢节奏讲解"
    }
  ]
}
In [119]:
# Cell 9 
from datetime import datetime, timezone

def recommend_publish_plan(audience_profile):
    now = datetime.now(timezone.utc)
    plan = {
        "primary_time": (now.replace(hour=20, minute=0)).isoformat(),
        "backup_time":  (now.replace(hour=12, minute=0)).isoformat(),
        "channel": "视频号",
        "frequency_per_week": 3,
        "tags": ["护肤", "敏感肌"],
        "reason": "目标受众晚间活跃,历史相似案例完成率高"
    }
    return plan

publish_plan = recommend_publish_plan(aud_profile)
save_json(publish_plan, "publish_plan.json")
show(publish_plan, "Publish Plan (module F)")
Saved /Users/vallenliu/demo_outputs/publish_plan.json

Publish Plan (module F)¶

{
  "primary_time": "2026-04-25T20:00:50.871893+00:00",
  "backup_time": "2026-04-25T12:00:50.871893+00:00",
  "channel": "视频号",
  "frequency_per_week": 3,
  "tags": [
    "护肤",
    "敏感肌"
  ],
  "reason": "目标受众晚间活跃,历史相似案例完成率高"
}
In [120]:
# 定义 A/B 判定函数(如果之前未定义或丢失,运行此 cell)
import numpy as np

def posterior_prob_B_gt_A(successes_A, trials_A, successes_B, trials_B,
                          alpha0=2.0, beta0=2.0, n_samples=20000, seed=42):
    rng = np.random.default_rng(seed)
    a_A = alpha0 + int(successes_A)
    b_A = beta0 + int(trials_A) - int(successes_A)
    a_B = alpha0 + int(successes_B)
    b_B = beta0 + int(trials_B) - int(successes_B)
    samples_A = rng.beta(a_A, b_A, size=n_samples)
    samples_B = rng.beta(a_B, b_B, size=n_samples)
    posterior_prob = float(np.mean(samples_B > samples_A))
    mean_A = a_A / (a_A + b_A)
    mean_B = a_B / (a_B + b_B)
    return posterior_prob, mean_A, mean_B

def decide_ab(successes_A, trials_A, successes_B, trials_B,
              probe_threshold=0.80, probe_rel_improve=0.10,
              strong_threshold=0.95, strong_rel_improve=0.15,
              immediate_fail_prob=0.05, immediate_drop_rel=0.10,
              alpha0=2.0, beta0=2.0, n_samples=20000, seed=42):
    posterior_prob, mean_A, mean_B = posterior_prob_B_gt_A(
        successes_A, trials_A, successes_B, trials_B, alpha0, beta0, n_samples, seed
    )
    rel_improve = (mean_B / mean_A - 1) if mean_A > 0 else float('inf')
    if posterior_prob >= strong_threshold and rel_improve >= strong_rel_improve:
        decision = "STRONG_PASS (full rollout)"
    elif posterior_prob >= probe_threshold and rel_improve >= probe_rel_improve:
        decision = "PROBE_PASS (increase traffic)"
    elif posterior_prob <= immediate_fail_prob and rel_improve <= -immediate_drop_rel:
        decision = "IMMEDIATE_FAIL (rollback)"
    else:
        decision = "UNCERTAIN (extend or collect more data)"
    return {
        "posterior_prob_B_gt_A": posterior_prob,
        "mean_A": mean_A,
        "mean_B": mean_B,
        "relative_improvement": rel_improve,
        "decision": decision
    }
In [121]:
# Cell 10 (替换版) — 小流量 A/B 实验模拟并显示结果
# 假设已在之前的 cell 定义了 decide_ab(...)
# 可修改 succ/trials 数值做多次实验

# 示例观测数据(可编辑)
succ_A = 120    # 控制组成功数(如完成观看/互动数)
trials_A = 4000 # 控制组展示数
succ_B = 150    # 变体组成功数
trials_B = 4000 # 变体组展示数

# 调用判定
result = decide_ab(succ_A, trials_A, succ_B, trials_B,
                   probe_threshold=0.80, probe_rel_improve=0.10,
                   strong_threshold=0.95, strong_rel_improve=0.15,
                   immediate_fail_prob=0.05, immediate_drop_rel=0.10,
                   alpha0=2.0, beta0=2.0, n_samples=30000, seed=123)

# 输出结果
from IPython.display import Markdown, display
display(Markdown("## A/B 判定结果示例"))
import json
print(json.dumps(result, ensure_ascii=False, indent=2))

A/B 判定结果示例¶

{
  "posterior_prob_B_gt_A": 0.9682666666666667,
  "mean_A": 0.030469530469530468,
  "mean_B": 0.03796203796203796,
  "relative_improvement": 0.24590163934426235,
  "decision": "STRONG_PASS (full rollout)"
}
In [122]:
# Cell 11
def simple_replay(material, metrics):
    # naive importance: check which factor correlated; here we mock output
    report = {"asset_id":"demo_asset_001","metrics":metrics,
              "factor_contribution":[{"factor":"hook","impact":0.4},{"factor":"cover","impact":0.3},{"factor":"cta","impact":0.3}],
              "actions":[{"action":"保留hook变体","confidence":0.8},{"action":"替换封面尝试AIGC","confidence":0.6}]}
    return report

# mock metrics after trial
metrics = {"views":5000,"completion_rate":0.32,"clicks":120,"conversions":12}
replay_report = simple_replay(material, metrics)
save_json(replay_report, "replay_report.json")
show(replay_report, "Replay Report (module H)")
Saved /Users/vallenliu/demo_outputs/replay_report.json

Replay Report (module H)¶

{
  "asset_id": "demo_asset_001",
  "metrics": {
    "views": 5000,
    "completion_rate": 0.32,
    "clicks": 120,
    "conversions": 12
  },
  "factor_contribution": [
    {
      "factor": "hook",
      "impact": 0.4
    },
    {
      "factor": "cover",
      "impact": 0.3
    },
    {
      "factor": "cta",
      "impact": 0.3
    }
  ],
  "actions": [
    {
      "action": "保留hook变体",
      "confidence": 0.8
    },
    {
      "action": "替换封面尝试AIGC",
      "confidence": 0.6
    }
  ]
}
In [123]:
# Cell 12
def gen_business_package(koc_profile, candidate, replay_report):
    pkg = {"koc_id":koc_profile["id"],"candidate_id":candidate["id"],
           "summary":{"expected_reach":5000,"expected_conversion_range":[10,30]},
           "cases":[{"case":"相似KOC转化3.2%","link":""}],
           "price_suggestion":{"type":"cpm_or_cpa","suggested_min":500},
           "notes":"含样品/流量包建议"}
    return pkg

business_package = gen_business_package(koc_profile, candidate, replay_report)
save_json(business_package, "business_package.json")
show(business_package, "Business Package (module I)")
Saved /Users/vallenliu/demo_outputs/business_package.json

Business Package (module I)¶

{
  "koc_id": "koc_demo_001",
  "candidate_id": "sku123",
  "summary": {
    "expected_reach": 5000,
    "expected_conversion_range": [
      10,
      30
    ]
  },
  "cases": [
    {
      "case": "相似KOC转化3.2%",
      "link": ""
    }
  ],
  "price_suggestion": {
    "type": "cpm_or_cpa",
    "suggested_min": 500
  },
  "notes": "含样品/流量包建议"
}
In [124]:
# Cell 13 
from datetime import datetime, timezone

def compliance_check(texts, assets):
    # naive rules: block if words contain '治愈' 或 '保证'
    blockers = ["治愈", "保证"]
    issues = []
    for t in texts:
        for b in blockers:
            if b in t:
                issues.append({"type": "blocker", "text": t, "reason": f"包含敏感词 {b}"})
    status = "pass" if not issues else "require_approval"
    return {"status": status, "issues": issues}

texts_to_check = [s["body"] for s in scripts]
audit = compliance_check(texts_to_check, material)
audit_record = {
    "asset": "demo_asset_001",
    "audit": audit,
    "timestamp": datetime.now(timezone.utc).isoformat()
}
save_json(audit_record, "audit.json")
show(audit_record, "Compliance Audit (module J)")
Saved /Users/vallenliu/demo_outputs/audit.json

Compliance Audit (module J)¶

{
  "asset": "demo_asset_001",
  "audit": {
    "status": "pass",
    "issues": []
  },
  "timestamp": "2026-04-25T02:35:53.322577+00:00"
}
In [125]:
# Cell 14 的内容(带时区)
from datetime import datetime, timezone

final_package = {
    "meta": {"generated_at": datetime.now(timezone.utc).isoformat()},
    "module_A_koc_profile": koc_profile,
    "module_B_audience_profile": aud_profile,
    "module_C_candidates": candidates_list,
    "module_D_scripts": scripts,
    "module_E_material": material,
    "module_F_publish_plan": publish_plan,
    "module_G_activity": activity,
    "module_H_replay": replay_report,
    "module_I_business_package": business_package,
    "module_J_audit": audit_record
}

save_json(final_package, "final_package.json")
show(final_package, "Final Package (all modules)")
Saved /Users/vallenliu/demo_outputs/final_package.json

Final Package (all modules)¶

{
  "meta": {
    "generated_at": "2026-04-25T02:35:53.824806+00:00"
  },
  "module_A_koc_profile": {
    "id": "koc_demo_001",
    "name": "Demo KOC",
    "style_vector": {
      "Humor": {
        "val": 45.0,
        "conf": 0.5
      },
      "Emotionality": {
        "val": 50.0,
        "conf": 0.5
      },
      "Rationality": {
        "val": 26.7,
        "conf": 0.5
      },
      "Pace": {
        "val": 58.3,
        "conf": 0.5
      },
      "Spontaneity": {
        "val": 71.7,
        "conf": 0.5
      }
    },
    "capability": {
      "shoot": 60,
      "edit": 50,
      "improv": 40
    },
    "preferences": {
      "categories": [
        "护肤",
        "美妆"
      ],
      "price_range": "low"
    },
    "consent_flags": {
      "orders": false,
      "followers": false
    },
    "created_at": "2026-04-25T02:35:47.882233+00:00"
  },
  "module_B_audience_profile": {
    "koc_id": "koc_demo_001",
    "segments": [
      {
        "id": "s_demo",
        "label": "默认年轻女性",
        "age_range": [
          18,
          34
        ],
        "gender": "F",
        "interest_tags": [
          "护肤",
          "美妆"
        ],
        "conf": 0.45,
        "sample_size": 80
      }
    ],
    "suggested_test_plan": "Run 2 variants in private group, 500 impressions each"
  },
  "module_C_candidates": [
    {
      "id": "sku123",
      "name": "护肤X霜(敏感型)",
      "brand": "小众牌A",
      "price": 79.0,
      "source_tag": "护肤",
      "evidence": [
        {
          "type": "ecom_trend",
          "text": "近30天搜索↑45%",
          "url": ""
        }
      ],
      "supply_signals": {
        "can_sample": true,
        "sample_time_days": 5,
        "inventory": 800,
        "lead_time_days": 3
      },
      "margin_pct": 0.35,
      "brand_score": 78,
      "supply_score": 65.9,
      "audience_match_score": 100.0,
      "rank_score": 80.6
    }
  ],
  "module_D_scripts": [
    {
      "id": "scr_sku123_0",
      "hook": "最近用了这个,效果挺让我惊喜。",
      "body": "产品:护肤X霜(敏感型),价格79.0元,卖点:近30天搜索↑45%",
      "cta": "点击链接了解/私信拿优惠",
      "style_label": "温和可信",
      "target_segment": "s_demo",
      "evidence": [
        {
          "type": "ecom_trend",
          "text": "近30天搜索↑45%",
          "url": ""
        }
      ]
    },
    {
      "id": "scr_sku123_1",
      "hook": "性价比超高,值得一试。",
      "body": "产品:护肤X霜(敏感型),价格79.0元,卖点:近30天搜索↑45%",
      "cta": "点击链接了解/私信拿优惠",
      "style_label": "中性推荐",
      "target_segment": "s_demo",
      "evidence": [
        {
          "type": "ecom_trend",
          "text": "近30天搜索↑45%",
          "url": ""
        }
      ]
    },
    {
      "id": "scr_sku123_2",
      "hook": "真心推荐,尤其适合你们。",
      "body": "产品:护肤X霜(敏感型),价格79.0元,卖点:近30天搜索↑45%",
      "cta": "点击链接了解/私信拿优惠",
      "style_label": "情绪化种草",
      "target_segment": "s_demo",
      "evidence": [
        {
          "type": "ecom_trend",
          "text": "近30天搜索↑45%",
          "url": ""
        }
      ]
    }
  ],
  "module_E_material": {
    "storyboard": [
      {
        "start": 0,
        "end": 20,
        "action": "钩子",
        "camera": "近景",
        "dialogue": "第1段示例台词",
        "cues": "表情"
      },
      {
        "start": 20,
        "end": 40,
        "action": "核心卖点",
        "camera": "中景",
        "dialogue": "第2段示例台词",
        "cues": "展示细节"
      },
      {
        "start": 40,
        "end": 60,
        "action": "结尾CTA",
        "camera": "特写",
        "dialogue": "第3段示例台词",
        "cues": "强调CTA"
      }
    ],
    "subtitles": [
      {
        "index": 1,
        "start": "00:00:00",
        "end": "00:00:20",
        "text": "第1段示例台词"
      },
      {
        "index": 2,
        "start": "00:00:20",
        "end": "00:00:40",
        "text": "第2段示例台词"
      },
      {
        "index": 3,
        "start": "00:00:40",
        "end": "00:00:60",
        "text": "第3段示例台词"
      }
    ],
    "cut_list": [
      {
        "start": 0,
        "end": 20,
        "reason": "钩子",
        "replace_with": "b-roll_closeup"
      },
      {
        "start": 20,
        "end": 40,
        "reason": "核心卖点",
        "replace_with": "b-roll_closeup"
      },
      {
        "start": 40,
        "end": 60,
        "reason": "结尾CTA",
        "replace_with": "b-roll_closeup"
      }
    ],
    "makeup_plan": {
      "level": "推荐",
      "steps": [
        "清洁",
        "保湿",
        "轻薄粉底",
        "自然腮红"
      ],
      "color_palette": [
        "#F2C1C1",
        "#FFF6EA"
      ],
      "confidence": 0.78
    },
    "covers": [
      {
        "url": "cover1.png",
        "prompt": "面部特写+大字钩子",
        "ctr_score": 0.12,
        "uncertainty": 0.03
      }
    ],
    "variants": [
      {
        "id": "v1",
        "desc": "快节奏钩子"
      },
      {
        "id": "v2",
        "desc": "慢节奏讲解"
      }
    ]
  },
  "module_F_publish_plan": {
    "primary_time": "2026-04-25T20:00:50.871893+00:00",
    "backup_time": "2026-04-25T12:00:50.871893+00:00",
    "channel": "视频号",
    "frequency_per_week": 3,
    "tags": [
      "护肤",
      "敏感肌"
    ],
    "reason": "目标受众晚间活跃,历史相似案例完成率高"
  },
  "module_G_activity": {
    "activity_id": "act_1777052783",
    "type": "reply_lottery",
    "title": "评论抽奖-试用",
    "trigger": {
      "time_window_hours": 1,
      "keyword": "#我想试",
      "start_time": "2026-04-25T01:46:23.125706"
    },
    "reward": {
      "type": "sample",
      "quantity": 5
    },
    "eligibility": {
      "min_followers": 0
    },
    "automation": {
      "auto_collect": true
    },
    "status": "active"
  },
  "module_H_replay": {
    "asset_id": "demo_asset_001",
    "metrics": {
      "views": 5000,
      "completion_rate": 0.32,
      "clicks": 120,
      "conversions": 12
    },
    "factor_contribution": [
      {
        "factor": "hook",
        "impact": 0.4
      },
      {
        "factor": "cover",
        "impact": 0.3
      },
      {
        "factor": "cta",
        "impact": 0.3
      }
    ],
    "actions": [
      {
        "action": "保留hook变体",
        "confidence": 0.8
      },
      {
        "action": "替换封面尝试AIGC",
        "confidence": 0.6
      }
    ]
  },
  "module_I_business_package": {
    "koc_id": "koc_demo_001",
    "candidate_id": "sku123",
    "summary": {
      "expected_reach": 5000,
      "expected_conversion_range": [
        10,
        30
      ]
    },
    "cases": [
      {
        "case": "相似KOC转化3.2%",
        "link": ""
      }
    ],
    "price_suggestion": {
      "type": "cpm_or_cpa",
      "suggested_min": 500
    },
    "notes": "含样品/流量包建议"
  },
  "module_J_audit": {
    "asset": "demo_asset_001",
    "audit": {
      "status": "pass",
      "issues": []
    },
    "timestamp": "2026-04-25T02:35:53.322577+00:00"
  }
}
In [126]:
# Cell 15
display(Markdown("### 输出文件列表(demo_outputs 目录)"))
for p in sorted(OUT_DIR.iterdir()):
    display(Markdown(f"- {p.name} ({p.stat().st_size} bytes)"))

输出文件列表(demo_outputs 目录)¶

  • activity.json (398 bytes)
  • activity.json.bak (398 bytes)
  • audience_profile.json (391 bytes)
  • audience_profile.json.bak (391 bytes)
  • audit.json (139 bytes)
  • audit.json.bak (139 bytes)
  • business_package.json (370 bytes)
  • business_package.json.bak (370 bytes)
  • candidates_list.json (614 bytes)
  • candidates_list.json.bak (614 bytes)
  • conversion_report.json (1307 bytes)
  • conversion_report.json.bak (1126 bytes)
  • final_package.json (7426 bytes)
  • final_package.json.bak (7426 bytes)
  • koc_profile.json (687 bytes)
  • koc_profile.json.bak (687 bytes)
  • material_package.json (1814 bytes)
  • material_package.json.bak (1814 bytes)
  • product_candidate.json (409 bytes)
  • product_candidate.json.bak (409 bytes)
  • publish_plan.json (282 bytes)
  • publish_plan.json.bak (282 bytes)
  • questionnaire.json (6873 bytes)
  • questionnaire.json.bak (6873 bytes)
  • recheck_encoding.json (1488 bytes)
  • recheck_encoding.json.bak (1285 bytes)
  • replay_report.json (516 bytes)
  • replay_report.json.bak (516 bytes)
  • scripts.json (1407 bytes)
  • scripts.json.bak (1407 bytes)
In [127]:
from pathlib import Path
OUT_DIR = Path("demo_outputs")
if not OUT_DIR.exists():
    raise SystemExit(f"Directory not found: {OUT_DIR.resolve()}")
json_files = sorted([p for p in OUT_DIR.iterdir() if p.suffix.lower()=='.json'])
print("Found json files:")
for p in json_files:
    print("-", p.name)
Found json files:
- activity.json
- audience_profile.json
- audit.json
- business_package.json
- candidates_list.json
- conversion_report.json
- final_package.json
- koc_profile.json
- material_package.json
- product_candidate.json
- publish_plan.json
- questionnaire.json
- recheck_encoding.json
- replay_report.json
- scripts.json
In [ ]:
# Cell 16
import chardet
from shutil import copy2

det_results = {}
for p in json_files:
    raw = p.read_bytes()
    det = chardet.detect(raw)
    det_results[p.name] = det
    bak = p.with_suffix(p.suffix + ".bak")
    if not bak.exists():
        copy2(p, bak)  # binary copy
        print(f"Backup created: {bak.name}")
    else:
        print(f"Backup exists: {bak.name}")
print("\nDetection results:")
for name, d in det_results.items():
    print(name, "->", d)
In [ ]:
# Cell 17
def try_convert_to_utf8(p: Path, detected_encoding=None):
    candidates = []
    if detected_encoding:
        candidates.append(detected_encoding)
    # add common fallbacks
    candidates.extend(["utf-8", "gbk", "gb2312", "latin1", "iso-8859-1"])
    tried = set()
    for enc in candidates:
        if not enc or enc in tried:
            continue
        tried.add(enc)
        try:
            text = p.read_text(encoding=enc, errors="strict")
            # if successful, write back as UTF-8
            p.write_text(text, encoding="utf-8")
            return {"status":"converted","used_encoding":enc}
        except Exception as e:
            # try next
            last_error = e
    # as last resort, read with replace and write utf-8 (preserves content but may lose chars)
    text = p.read_text(encoding=candidates[-1], errors="replace")
    p.write_text(text, encoding="utf-8")
    return {"status":"converted_with_replace","used_encoding":candidates[-1], "note":str(last_error)}
In [ ]:
# Cell 18
conversion_report = {}
for p in json_files:
    det = det_results.get(p.name, {})
    enc = det.get("encoding")
    print(f"Converting {p.name}, detected: {det}")
    res = try_convert_to_utf8(p, detected_encoding=enc)
    conversion_report[p.name] = res
    print(" ->", res)
save_report = OUT_DIR / "conversion_report.json"
save_report.write_text(__import__("json").dumps(conversion_report, ensure_ascii=False, indent=2), encoding="utf-8")
print("Saved conversion_report.json")
In [ ]:
# Cell 19
recheck = {}
for p in json_files:
    raw = p.read_bytes()
    det = chardet.detect(raw)
    recheck[p.name] = det
print("Re-detection results after conversion:")
for name, d in recheck.items():
    print(name, "->", d)
# save recheck
(OUT_DIR / "recheck_encoding.json").write_text(__import__("json").dumps(recheck, ensure_ascii=False, indent=2), encoding="utf-8")
In [ ]:
# Cell 20
import zipfile
zip_name = Path("demo_outputs.zip")
with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as z:
    for p in sorted(OUT_DIR.iterdir()):
        # skip the zip if exists in same dir to avoid recursion
        if p == zip_name:
            continue
        z.write(p, arcname=p.name)
print("Created zip:", zip_name.resolve())
In [ ]:
# Cell 21
import json
print("det_results:")
print(json.dumps(__import__("json").loads((OUT_DIR/"conversion_report.json").read_text(encoding="utf-8")), ensure_ascii=False, indent=2))
# 以及 recheck
print("recheck:")
print(json.dumps(__import__("json").loads((OUT_DIR/"recheck_encoding.json").read_text(encoding="utf-8")), ensure_ascii=False, indent=2))
In [ ]:
# Cell 22
p = Path("demo_outputs/final_package.json")   # 改为你遇问题的文件名
raw = p.read_bytes()
print("first 200 raw bytes:", raw[:200])
# 尝试用不同编码查看前 500 字符(不会写回)
for enc in ["utf-8","gbk","gb2312","latin1","iso-8859-1"]:
    try:
        print(f"\n--- try {enc} ---")
        print(p.read_text(encoding=enc)[:500])
    except Exception as e:
        print(f"fail {enc}: {e}")
In [ ]:
# Cell 23
bak = Path("demo_outputs/final_package.json.bak")
print("bak exists:", bak.exists(), "size:", bak.stat().st_size if bak.exists() else None)
print("orig size:", p.stat().st_size)
In [ ]:
# Cell 24
# 强制安全转换(已备份前提)
text = p.read_bytes().decode("latin1", errors="replace")   # latin1 保证不会抛异常
p.write_text(text, encoding="utf-8")
print("force-converted using latin1->utf-8 (with replace).")
In [ ]:
# Cell 25
from pathlib import Path
import json  # Import json as a separate module

p = Path("demo_outputs/final_package.json")
print("size:", p.stat().st_size)
print(json.dumps(json.loads(p.read_text(encoding="utf-8")), ensure_ascii=False, indent=2)[:1000])
In [ ]:
# Cell 26
import zipfile, pathlib
OUT_DIR = pathlib.Path("demo_outputs")
zip_name = pathlib.Path("demo_outputs.zip")
with zipfile.ZipFile(zip_name, "w", zipfile.ZIP_DEFLATED) as z:
    for f in sorted(OUT_DIR.iterdir()):
        if f == zip_name: continue
        z.write(f, arcname=f.name)
print("zip created:", zip_name.resolve())
In [ ]:
# Cell 27
display(Markdown("""
## 说明:如何把这个 Demo 分享或在网页上呈现

- 你现在有 demo_outputs/*.json 与 demo_outputs.zip。上传这些文件与本 Notebook 到 GitHub 后:
  - 评审可以直接在 GitHub 上查看 JSON 文件;或
  - 使用 Binder(https://mybinder.org/)或 Google Colab 打开 Notebook 实时运行交互演示。

- 若要把 Demo 做成静态前端页面(可供非技术评审直接交互):
  1. 使用之前给你的 web/index.html(示例)读取 demo_outputs/*.json 并渲染页面(前端无需后端支持)。
  2. 将 web/ 与 demo_outputs/ 放到 GitHub 仓库并启用 GitHub Pages(或部署到 Vercel/Netlify),浏览器即可访问静态演示页。

- 若要做成真实产品演示(支持真实 LLM/ASR/图像生成与发布):
  - 需要把 Notebook 中的模拟函数替换为后端服务(FastAPI)并接入实际模型与外部 API(注意合规与授权)。
"""))
In [ ]:
# Cell 28
display(Markdown("""
### Demo 完成 —— 下一步建议(选项)
1. 我可以把本 Notebook 与 web/index.html、必要静态 JSON 打包为 GitHub 仓库模板(你可直接 clone)。  
2. 我可以把某些模块的模拟函数替换为真实 API 调用示例(例如:调用 OpenAI 生成分镜、调用 Whisper 做 ASR,或调用 SD 做封面图)。  
3. 我可以生成一个简单的静态前端(读取 demo_outputs/final_package.json)并部署到 GitHub Pages,供非技术评审直接访问。  

告诉我你想要哪一项,我马上为你生成对应的文件/部署步骤。
"""))
In [ ]: