#!/usr/bin/env python3
# SPDX-License-Identifier: CC-BY-4.0
# Creator: speech-technology-editor; source: AI Encyclopedia CN, Chapter 15.
"""Deterministically generate or check Chapter 15 sampling, framing, WER and CTC data."""
import argparse, cmath, json, math
from pathlib import Path

FS=8000; F0=1000; N=16
REFERENCE=("请","把","灯","打开","然后","离开")
HYPOTHESIS=("请","请","把","灯","打开","离去")
BLANK="<blank>"
CTC_STEPS=({BLANK:0.6,"A":0.4},{BLANK:0.3,"A":0.7},{BLANK:0.5,"A":0.5})

def samples(): return [math.sin(2*math.pi*F0*n/FS) for n in range(N)]
def dft(xs): return [sum(x*cmath.exp(-2j*math.pi*k*n/len(xs)) for n,x in enumerate(xs)) for k in range(len(xs))]
def wer(ref=REFERENCE,hyp=HYPOTHESIS):
    # Cost plus deterministic operation counts. Tie order: diagonal, deletion, insertion.
    dp=[[(0,0,0,0) for _ in range(len(hyp)+1)] for _ in range(len(ref)+1)]
    for i in range(1,len(ref)+1): dp[i][0]=(i,0,i,0)
    for j in range(1,len(hyp)+1): dp[0][j]=(j,0,0,j)
    for i in range(1,len(ref)+1):
      for j in range(1,len(hyp)+1):
        if ref[i-1]==hyp[j-1]: diag=dp[i-1][j-1]
        else:
          c,s,d,ins=dp[i-1][j-1];diag=(c+1,s+1,d,ins)
        c,s,d,ins=dp[i-1][j];delete=(c+1,s,d+1,ins)
        c,s,d,ins=dp[i][j-1];insert=(c+1,s,d,ins+1)
        dp[i][j]=min((diag,delete,insert),key=lambda z:z[0])
    return dp[-1][-1]
def collapse(path):
    merged=[]
    for x in path:
      if not merged or x!=merged[-1]: merged.append(x)
    return tuple(x for x in merged if x!=BLANK)
def ctc_paths():
    out=[]
    for mask in range(8):
      p=tuple("A" if mask&(1<<t) else BLANK for t in range(3))
      prob=math.prod(CTC_STEPS[t][p[t]] for t in range(3))
      if collapse(p)==("A",): out.append((p,prob))
    return out
def ctc_forward_target_a():
    # Extended target [blank,A,blank]. Standard CTC recurrence.
    ext=(BLANK,"A",BLANK); prev=[CTC_STEPS[0][BLANK],CTC_STEPS[0]["A"],0.0]
    rows=[prev]
    for t in range(1,3):
      cur=[]
      for s,label in enumerate(ext):
        total=prev[s]+(prev[s-1] if s>0 else 0.0)
        if s>1 and label!=BLANK and label!=ext[s-2]: total+=prev[s-2]
        cur.append(total*CTC_STEPS[t][label])
      prev=cur;rows.append(cur)
    return rows,prev[-1]+prev[-2]
def build_payload():
    xs=samples(); X=dft(xs); cost,S,D,I=wer(); paths=ctc_paths();rows,forward=ctc_forward_target_a();total=sum(p for _,p in paths)
    assert (cost,S,D,I)==(3,1,1,1)
    assert math.isclose(total,0.85,rel_tol=0,abs_tol=1e-15) and math.isclose(forward,total,rel_tol=0,abs_tol=1e-15)
    assert math.isclose(abs(X[2]),8,abs_tol=1e-12) and math.isclose(abs(X[14]),8,abs_tol=1e-12)
    return {"schema":1,"creator":"speech-technology-editor","source":"AI Encyclopedia CN, Chapter 15","license":"CC BY 4.0","license_url":"https://creativecommons.org/licenses/by/4.0/","provenance":"Original synthetic sine and fictional token sequences; no people, recording, external corpus or deployment data.","conventions":{"samples":"x[n]=sin(2*pi*f0*n/fs), n=0..15; seconds and hertz","dft":"X[k]=sum_n x[n] exp(-j*2*pi*k*n/N); forward transform unnormalized, inverse has 1/N","floating_point":"IEEE-754 binary64; near-zero DFT residue retained","framing":"left-closed/right-open [mH,mH+F); complete frames only; no tail padding","wer":"minimum token edit distance divided by six reference tokens; tie order diagonal, deletion, insertion","ctc":"merge adjacent repeats first, then delete <blank>; path product; target probability is path sum; loss is unnormalized natural logarithm in nat"},"sampling":{"sample_rate_hz":FS,"tone_frequency_hz":F0,"sample_count":N,"sample_period_s":1/FS,"nyquist_frequency_hz":FS/2,"dft_bin_spacing_hz":FS/N,"samples":xs,"dft":[{"k":k,"frequency_hz":k*FS/N,"real":z.real,"imag":z.imag,"magnitude":abs(z)} for k,z in enumerate(X)]},"framing":{"total_samples":16000,"sample_rate_hz":16000,"frame_length_samples":400,"frame_length_ms":25,"hop_samples":160,"hop_ms":10,"padding":False,"complete_frame_count":98,"last_frame_half_open":[15520,15920],"tail_samples":80},"wer":{"reference_tokens":list(REFERENCE),"hypothesis_tokens":list(HYPOTHESIS),"substitutions":S,"deletions":D,"insertions":I,"reference_length":len(REFERENCE),"wer":cost/len(REFERENCE)},"ctc":{"alphabet":["A"],"blank":BLANK,"time_steps":list(CTC_STEPS),"valid_paths":[{"path":list(p),"probability":q} for p,q in paths],"target_probability_by_enumeration":total,"forward_rows":rows,"target_probability_by_forward":forward,"loss_negative_natural_log_nat":-math.log(total),"length_normalized":False}}
def encode(): return (json.dumps(build_payload(),ensure_ascii=False,indent=2,sort_keys=True)+"\n").encode()
def default_output_dir():
    here=Path(__file__).resolve()
    if here.parent.name=="data" and here.parent.parent.name=="public": return here.parent
    return here.parents[1]/"public"/"data"
def main():
    p=argparse.ArgumentParser(description=__doc__);m=p.add_mutually_exclusive_group(required=True);m.add_argument("--check",action="store_true");m.add_argument("--write",action="store_true");p.add_argument("--output-dir",type=Path);a=p.parse_args();o=(a.output_dir or default_output_dir())/"ch15-speech-case.json";b=encode()
    if a.check:
      if not o.is_file() or o.read_bytes()!=b: raise SystemExit(f"FAIL: {o} missing or differs")
      print("PASS: Chapter 15 sampling/DFT, framing, WER, CTC enumeration/forward and logarithm are deterministic")
    else:o.parent.mkdir(parents=True,exist_ok=True);o.write_bytes(b);print(f"WROTE: {o}")
if __name__=="__main__":main()
