AJOnxSdFet#3b(QFaZB=
ziy{U_7uKt0J}Uy9B?SE?K=z|o(7gcJ#W0Nq;i+lf&75;aBfCqWoFK0p5cTca1vw-Q
zk4J*pJvP5+nW
z$k}<(6?S=LqBttg7<1F7efUJ9jiKunOSAMICu`G;C5<;T!Y
zYMGrQx%jhgvZ%qV9vV|uUnCNYP-S${UykBJp`x^er{^qFtJHE}fy%fRxq{M0$)ie|
z7Be7#^>p<9@+&gcGVtOr+N`_`UnWfmD%J~ZZh>vRpdwX1W*m)-7$X#nMa%`|Qohb9
zcXM*r((>KpfmPQcBd)}kXi9)I;V{e2GlrSN%IvH|?1RGeY;Ea-Qe6&ff#TvaG}4u^
zXy-XDE(?ZLd^k=!nV1(@)(*ymFV>z=v07v6jKF3@J+;m99GY=lx*uSW
z{6k&;ET7SbjGY7x#gVDOXZjBJ!PD5j%`O4M!{C|4U@yoQ_UX^7hUdW`4(|i|#`y1~
z&BNDZP;frCbbNw4F>#C8x22ChfBAytz~kfO(w)3;KR?$%Pc^TybS`FW^^Q4#5sKcx
zHC>`Tj!O2P08uFvQdua4i-|VQI*XXc)goA-Zx~;uehNZ{evq`fm|^ByLgR4+0Ya@bD>S}l;{H6}9saaF0tQtt>vbnuaES5U6#biRwW-s?t
zQ<}_95bR-3v0sVU2iTG`3Aee&SxqJwiEv(+T+MTzU*t_
zTKjV1@`a>-ioPbUu`eSoTSx}ks@CRK)t5owzt9nrc;_Q$9Wi|-izX1oM#(D5$#aiD
zhh56!lT4a9(k{yLoMx_IX;x9R&UhCruhryhn#Ky2=kjz1AXULQSJe-lybG4u>HsF7
zRgMA~;*y1j<3Cr^_xR
zf*-&h#-0Ag*EG1(ogyj&i}`Pq7Z9G@&h(r?G63^;@#}WIb&v&f|EtGe{|jc|+2zon
zN(~JS9<^$bhk6tuF`kM6*~nB2lSQ=-jXNvIR4a=~wY=?tSIJoWG#I32q?b;Ud`=Z7SW`gI^!Ylt!>9dR&?i%q4wfej7kcIC{_rfI5
z5E6$cQGwCWI$gSKAA`vqyQ)P`SV`_gt1D?M)5x88xvqcMLvD~&PTs@_a-*%5$gMP=
z8+F+xpWuEc=r&v)_;wkp1f3dab51Zx=SDt
zjrvFR8>YRRb)5R{-@Z91Xe1fUXw&gI
zT%7
zsC9%kT&?nbJfX>rlp=U^Z~^r&-?vcoZ~5p?u0_a0tCQaA$?->wF1S%&VFX4PHz??F
z2okS$-oof|)U24}JcsvS+@PT9V2|t!E{V=%IXmqr+
zHt`^whHjzqbTWfhnYlZyzc)RzoPZ!i>%s7&B}6Ru=TB}X+HUiI7(T1f@b9H##Tfp*
zfVbn}M-#RU8KUfwB@F+CO*r_zCt^0a;ZI@NlGpK>{HwqksATX%=licnP-me=g5VWi
zauZU>wyG5ETD`nut-KvYw_V)0x~WRGpDQHNg8{c#oI2#N`9*}}G;c{>#{3IDGH{d~BoU!-h=Hl7C9m8M;7
zkYFw)*6yX7!H(I}uFrYcO^@qK5g2lQiYgHouDmmwN4fYZJrR>FT^L={&G4Fh6aVc<
zc@xX6eOye2`)cvO^^?^;HikGC+DeOqN0}?X7?8dr4*i}5M|YBRWyh6;I8V)~ae?8E
z0g2q3qfC)IDKB8)(kyxY`uMqXqJ$KgT4Qo|bsVQTw9Ulc(bmr1Ih3q5eRj6Dt&e9)
z&`HHPBL-v6Y~epphw#rf8U%tW!R)F7@By51KfE8v_dBNGDIpK`SWleNEC@JNb9vyd
zY2qW9xjQ!%fQ;FZ_Duk`pAI}GV8%plfw!IZjmNOOvAcnq?+Uc3;9j_JVhvgVz_dB?
zH~2TB2U1aN1JLg;C5H}2IKj;;E)NZtW5x}>X2`=uQ^dmsIV*Xuo%nTrXBHeLPF27=
zU-MSx$iX(#8o{X=nT)5x=u{Z*lc~K|yA2;N2N%hvb%)nvCS58A$Kl)2-qsgo+=@W?
z{9Db*CII7FzkdC)b*-CUuC%}GX4i4c_3i8Qx0XhnFRp(#-!G^B4>az9-xlnJFA;`c
z!QllXx`L6*@UI2Ku`s!Kfff)el{euR3F=TdKS3=~IpXBHc==7$1xJXM!;W}G!Rp!6
zn=`}~q>zg#>xvdrB+kVZn)_bKZ1w!#L{?|67t&
z)Pn2qI%)f$-Eds5g%uk3_?Pf8g%&1}P9Ml0|2H3%>$0A~{)NMISX&*ZKD)e`NF{*L
zFifN$s0)6Y=Z^>ciwHPd_K)a~*>XWb
zI+w=9g$F--%D-;U_qed)+EgPUn*?*qI4X@V--NT5Ih{<7xPinnF)1umSSTtoYs0?>
zf9^&Vw&0)t#;l369!r3BO51E@Puh=BT%i3D$-KU}VIp7jh*zxU8?~B?d%&u-0?qeR
zaLJ!V?vjT^@Jo+&O8flVP@x(*6>>TRh_)_wC$FRiZYG?$bcYvO;PF7$Ecf2m!R^Xb
z=mpX2fI}G>cl2B{+`N?L1f19Pr-2Hd1FZi3b7JsY>Zc|ifGbkL`}g$%ut^QNyW=-6
zCT!EEYSBr2$U3LmviUT4u@2uKsd(e3Oo;
z>iU-<91*(cjfL)#KZ{`Cff0~&Is`oZp0s32J9S?dG2qefk
z$mz`GMTQ(`3=i$)0Pp>1pO<#uW}PJV)!l7U_afjKfWsuVwp4bhn1-&d
zmQ?VLHki)!fjo?`&ggnh1$nqO=qg%X;?a#1;HP0_xDWJjVb6KZKJ{9w!TAo*)pbxU
z4GjN-jrhdS?{+R^(5uetuC0uidv~>|06v&e`cO-EfB)yNz5N8NZUsW&N%eC8gTF#t
z0PBs#y1cQQ@Xpto6**u*`ac5tA%*eAUrGOgK!$}eC_Ll!FPJ0XGf8w{u_eOvL!a`m
z6%v-2TX{FS`^49X9s=ods!l)tdE37IE-V)7?Z$fdx`EEIMn*759X47jW-q|w7c;t
zCDJlBd1EzrLb$;3q?T(Lnj9Tuzkl;e~t!0}iC=^-le(!rSQRcVH277Ca#2h!X<+O@Xn|Z{K|Z2;qgX{g2^C{a_?S
zAO=TJJ@Bu3oJ;=zJgXnjJ#V8wX49j0jHwDbV}Z_qSP>%ClUA
z0(HXhrS9ebknf7zM{4;;^tV+Z8GBXSvo8J?x9sjQc0OMQm3WmvRV^nxeM@ik_&&e||7ZE{XZBI
zy$K6lBw_LzZ9V~j3z|;6qjr3j?0WTLWyUwe|EljQh@q~MK9l_~SNs9`D;tHQbT1s+
zxVRU(Pu!#xRWM?J@0^?Xj*@c7=>quqF0>#Rl)dKVO|~=VuqE8-jW%$=I7wD*Ysxj)
za*bi_{j?##Wts-6Az_qBNDIr=Tl4>{7$IY|tt{s-0?N-!Ie_~P)X&XCqiJ#^jS
z)*~K$!(%9eeM^==%jCeE>s)GyG>}NELM1@@G?Poz
zq=m0BG%t1BH)%r$1n}C4>M1RM^62qqb98Wxw~l`#mSUPjTHw0f5@6FLh~#8PT1Mj(
zhD7QulWibJl!Tikur#H!rhem-F&m~J3m=heE8!*GY*HyVO)|wd&zOEhaf{ARoDj*h
z)w73E!8M1_C+_>X`?GcwqTCH0Ox>gBCUUl&dz)Q#S?uw~`OOF4{mW*Z49
zrJDutW0Lm*iJUtH@I6I98%K%SKy{r>6U3M?qzeWj?HU4d2(@bo_`nTz?O>7G#J;As
z@66R;4FDqIIZhJp?3w`{QC?G+ParV{ff3I#Dta-Z31I7cPG{XEZ6RD;cIA-RcNL*&Qjct{mQn|%OkQiF>XmtV_Z^ggv&3<#A|l@K#s`LGN9`v}pNan5
zeUc1G0D1*~%KCR5EH-G1pd?1tEZjH>GWin?F)cQ>&yo-{kWAN#jD}nXEwExy_j4MH?b
zpUIvllF%y|+`(#6;;K|p;Y~mGB3mbKP}6{1oUZSeorwnV+|s(?t~!J329777ZN55*
zITml?f7dV8h_fZ5Y%K2_qRX$~;)lIq2
zMFap{_gP5h{E>7%`!^-BGOa5;EY9Nj3jh-a)(8GoZ|)7pn!5kFy9W85MBHox
zBY#`Z!vL0S{Px%N+{b8&1P_v?n7A)YniFnXCY?q^?2vrP4WGvHd?TeMBNQ#3S#~q0
z#t>CCWvi)@)MT3wQm_*1ky7BYo3AD5pfnSjjU*)k^ithuZf3bE!wf4i9+E*58QMlW
zu-{<3;jVDoNfggZc|KrpG&Tjxl`7^GJF>bL*#tA}$)j`H!7R^9?k1ezYvKjS`keVQ
z>c{u5+W!(VmmA;NNIyYs9-Qqt{|)#^2gUeO@N8
zl^@DpmklBqvEnvSZ*tR|cWIX93-;mtQy)o-US(uoWb(ZVsPhFNd`?|ep&X=Txxo5f
zX1O92G#b*Ew5i@DzKTa~)ZT9P&sTm;o#~}CmJegJFL0A8)XDn48$jdcsH}Oue?X3^
z4t764yHThvnf$^kSt(gon%*tb_8Kj2PGo{DaZQ#$xE_N%30sY9$D&2X-9}Bf%u}<4
z@u|{Lik>D0Br?`^iNlecv@N4)XG%XxAL(u4Y2x9e=#}s-?9v7`a~-4$5n)-+F}a%F
zwr-5BY4rHmP<4?%9^YhAa^;VK6Ju^d=$bBC*Gzo$l29`_k)%hgrj@)3<~5RSAAy(L
zl~P|urAH>2DPE;(bFdw5Qxpk9YL>0UvAEQ2ZSs)(+?ZtU))mfeSqic!w0l6m6>R@FRP_v5*4uoI+cNPQ
zS5>las;buWE4NYYj&)m!+xH6nTym0PPu7i9Q7M^@nzGW>!KH~NHZBD$UE>EE);(fQ
zYOYy9E23fRnl`kBV!?6wAxqnRPA4X&p!SDqCJ;=g3a_liZCi1u=VI17PGKTTrg6?)
zu}XhO*8~lzIGq%u;YpHb;$4TKIy|jgjAih=vws6dBnXj$W!5AK&G;Y05tNj82;x;l-001xm1ObRK
zReGuGd1y11B^$G5H12qDHn~{19S=j-!VKKql|(&FvTD)5tuLd4M`wM+08x*BJ(|Nr
zlo)fWq0swEOlGc)0)w&4t_lf&aP(h5puDo8qK-?OrYswl)#ar7vfaW7flDeWVf~A(
zY*+`Tu5Ot*T`6PoXSn7vfj%5#M3SS}fVzjglvwK`1NU-SrN<$UvKqUQ^a>lhTy4-H
zwNrbuN~OYtx<%htgV7a?3>9ROs1NPJT=UofAkae?CDC~r34?KqObr=zG!JnnB707b
z7r03b%E59aY!TF^Tt{|;WOSrSC&U}8nn5J+VIaM|k`imAIV0Y#{I{d9>zQNyvagRm
zQK~sx(WJuI*V(T6_5#H$<5#PH^&
zC%rP5EH;PB;|qi$u|yh^YswW$m0F|K>3i^N8BJ!3)n+GxU-+P*W1^nEfuRwE_0oQP
z1;X0q7M4~{oI1nc+RBbwnShEOJ$Z&2dseN5;6)Z8ajp}&uzuQ#lt5c
zBqAmuB_pSxq@t#wrK6YTp=K^?rQO9X&NFmrWo=_?&(n@T{dqzZ+MtK>sHj_}P%*qM
z43nNRPGuU2R3--sC9+xbkT)3O?c?i5o6XQT>OS*>!19XaW804FVal%of?_y4fk+}#
zs5JT~sUOZpCRR3K*H}cX7PNs%G?VPSeB3k5#hVkywIO@9qsooG+?cL#H^!C|^>J
zAyH0R*(ts^8<>ikhL#RO4~;$-F){p;hlouEhy1`eh>4knmF+9!jw@Ahs8*8>YrY<6
z)}mFLb{#r(>CQ1f^y<@ZmH~r?3>yIe1Of&D1p|kGgo1|QT~oa2h)BpNsA%XI?g{H*
zTkx2GS}lv)?*URWatcZ+Y8qNPdIm-&W)@a9b`DN1ZXRCogRroOm4d}bkYo&wK*Fvm
zV-NQ4#ip%hyBPkMNIdHeYK`I9|x0XBB7+>nDw
z+M{V-1RvTplvUTgN@^%<%wMG=ar
z!*jQ+qV}xyN6`Kk59SQI2ZZ&QB3}cD_ROwR2c*1p)q4;L+Qi8t*UfLchY$&b+t;JKWu;*NKhV3Dn>ZoQe4?Z6Tw{f)zZvpK}nn5An=gFmz_bDHSczfes@l-LYq
z0;g4Xcg=%uCsTs7o4{GNWUa!pmac2H0Kd!cJm*N*)uZ!%osqZ*>LO^*ltnMmtH&3(
zcU?WEKYMK#Ejf=i=3;K3-K%fkMfaxjIf2{OrLPt4b_rVVYhJJI!{g>_cl{f-Ic)|*
ztswPTXlW~E&a9qvaUk}uyM7}K_B|f%u90pwrr!Pa>Q|v^Tu5b^*mFfi5r7Zr
z-;A`ijOcHxKUKbGB7_1mE~L_yvE6uCg#t1zq|%nL9XDP@-2eap00000fQX2Qh=_=Y
z$QWacF~%5Uj4{SJ=bUrSIp>^n-U5JDOimyR&n*;CNGm2Upc}7F)y5cOjA>gSUOp9_
zFHmVWy`z_Fr3<;E-_?d~0`F{jwOvl{f3jCj!9#0xoqc~RMW((yk9fBSZgP*GtQgvB
zlBxA!u(kxANL#?OvZMgxLaOLoLID{UQbpfh>j-(_01oqs2MuRb7
zySC+VS1aP21^>z0$m7j6+>PwjlZAiOM&|DhH95Y!i!1jF*#xD
zT?ArhwLaV>4V6n;7+mLGwMhvnaOY4@+ua2R)`9=6&X|W-g4#mDSoxaGGCB3?BTVz(ZXF
zOL%%1Y$l7q@OO2<;gP5jb?0e^(J=Mma#^Z5PChWkZq^JevkO}*V2>?_nC)@O-haEO
zk8gRyoN;Olz03QtU&uLEiJNM2Zd2YCnTW-#(wl3}Pp3WKQ_mAGs$YPvQ;4jfo*ikh
zSnNyp<3!!OUhT+6%w;y}2=>&~N_YxjU=fGT9BgVhDn>vny3vgu)q>^Lrkm!zD{5}*|5RZ9E(HLT!IcJX!UCC5lm
zIp;m`X;IEs!ZUBBa2rGR1_AsX(C+Q0xjj32`W@!cGbuv4VCT+U=_tI{q!ll9VeAX0pir-&=aH_nm)QD{K?`MQ{A+*FYj@G=ASRH0=tF
zIfsJ3O6%T{*k6aVCqL<#w~nilW-LIera6BRPwrYGN=_kTm9$z@HM!Q80OlCna2VOX@O&Gs;%EBGN=B=0|VNXsvlhV#v
zOd5~s334k0y*JFG+g|TVff3%>S9o%KDn>
z?WNYYT|zH;JG<>=j!}eftD1@eFFBQn@#bs?aCu}!YW^(Dp)q$QBVv0+XPZ`yD-FYT
zOio98defrSSUhn91%H|bTbw1yzqy7~bDZC?n0B+YpYbu)X$?*l8FcD{^Fx&BB+WaR2}S
literal 0
HcmV?d00001
diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg
new file mode 100644
index 0000000..47d58f9
--- /dev/null
+++ b/docs/assets/logo.svg
@@ -0,0 +1,4 @@
+
diff --git a/docs/assets/symbol.svg b/docs/assets/symbol.svg
new file mode 100644
index 0000000..4bf1e7d
--- /dev/null
+++ b/docs/assets/symbol.svg
@@ -0,0 +1,3 @@
+
diff --git a/docs/index.md b/docs/index.md
index 30f6351..ddda0a3 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,12 @@
-# gladia-normalization
+Open source · STT · WER
-Normalize speech-to-text transcripts before computing Word Error Rate (WER), so formatting differences stop looking like recognition errors.
+# Normalization
+
+
+Normalize speech-to-text transcripts before computing Word Error Rate, so formatting differences stop looking like recognition errors.
+
+
+
| Ground truth | STT output | Without normalization |
| --- | --- | --- |
@@ -13,6 +19,11 @@ Input: "It's $50.9 at 3:00PM — y'know, roughly."
Output: "it is 50 point 9 dollars at 3 pm you know roughly"
```
+[Get started](getting-started.md){ .md-button .md-button--primary }
+[How it works](concepts.md){ .md-button }
+
+
+
## Quick example
```python
diff --git a/docs/stylesheets/gladia.css b/docs/stylesheets/gladia.css
new file mode 100644
index 0000000..858f668
--- /dev/null
+++ b/docs/stylesheets/gladia.css
@@ -0,0 +1,494 @@
+/* Gladia design tokens — mapped onto MkDocs Material
+ * Source: gladia-design/DESIGN.md (dark-first, white contrast, purple spike)
+ * Suisse Intl is licensed — use the system stack that matches its metrics.
+ * Geist Mono (SIL OFL) is self-hosted for code / mono labels.
+ */
+
+@font-face {
+ font-family: "Geist Mono";
+ src: url("../assets/fonts/GeistMono-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+:root {
+ /* Primitives */
+ --gladia-neutral-0: #ffffff;
+ --gladia-neutral-100: #f5f5f5;
+ --gladia-neutral-200: #e5e5e5;
+ --gladia-neutral-300: #d4d4d4;
+ --gladia-neutral-400: #a3a3a3;
+ --gladia-neutral-500: #727272;
+ --gladia-neutral-600: #515151;
+ --gladia-neutral-700: #252525;
+ --gladia-neutral-800: #1c1c1e;
+ --gladia-neutral-900: #0c0c0c;
+ --gladia-neutral-1000: #000000;
+ --gladia-purple-400: #947afc;
+ --gladia-blue-400: #1a9eff;
+
+ --gladia-font-sans: "Suisse Intl", -apple-system, BlinkMacSystemFont, "Segoe UI",
+ Helvetica, Arial, sans-serif;
+ --gladia-font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
+
+ --gladia-radius-1: 8px;
+ --gladia-radius-2: 16px;
+ --gladia-radius-5: 40px;
+ --gladia-space-4: 16px;
+ --gladia-space-6: 24px;
+ --gladia-ease-out: cubic-bezier(0, 0, 0.2, 1);
+ --gladia-duration: 160ms;
+}
+
+/* ── Dark (default / Canvas) ─────────────────────────────────────────────── */
+
+[data-md-color-scheme="slate"] {
+ --md-default-fg-color: var(--gladia-neutral-0);
+ --md-default-fg-color--light: var(--gladia-neutral-400);
+ --md-default-fg-color--lighter: var(--gladia-neutral-500);
+ --md-default-fg-color--lightest: var(--gladia-neutral-600);
+ --md-default-bg-color: var(--gladia-neutral-1000);
+ --md-default-bg-color--light: var(--gladia-neutral-900);
+ --md-default-bg-color--lighter: var(--gladia-neutral-800);
+ --md-default-bg-color--lightest: var(--gladia-neutral-700);
+
+ --md-primary-fg-color: var(--gladia-neutral-1000);
+ --md-primary-fg-color--light: var(--gladia-neutral-900);
+ --md-primary-fg-color--dark: var(--gladia-neutral-1000);
+ --md-primary-bg-color: var(--gladia-neutral-0);
+ --md-primary-bg-color--light: var(--gladia-neutral-300);
+
+ --md-accent-fg-color: var(--gladia-purple-400);
+ --md-accent-fg-color--transparent: rgba(148, 122, 252, 0.1);
+ --md-accent-bg-color: var(--gladia-neutral-0);
+ --md-accent-bg-color--light: var(--gladia-neutral-300);
+
+ --md-code-fg-color: var(--gladia-neutral-200);
+ --md-code-bg-color: var(--gladia-neutral-900);
+ --md-code-hl-color: rgba(148, 122, 252, 0.18);
+
+ --md-typeset-a-color: var(--gladia-neutral-300);
+ --md-typeset-table-color: rgba(255, 255, 255, 0.14);
+ --md-typeset-table-color--light: rgba(255, 255, 255, 0.08);
+
+ --md-footer-bg-color: var(--gladia-neutral-1000);
+ --md-footer-bg-color--dark: var(--gladia-neutral-1000);
+ --md-footer-fg-color: var(--gladia-neutral-400);
+ --md-footer-fg-color--light: var(--gladia-neutral-0);
+ --md-footer-fg-color--lighter: var(--gladia-neutral-500);
+
+ --md-admonition-bg-color: var(--gladia-neutral-900);
+ --md-admonition-fg-color: var(--gladia-neutral-0);
+}
+
+/* ── Light (opt-in editorial band) ───────────────────────────────────────── */
+
+[data-md-color-scheme="default"] {
+ --md-default-fg-color: var(--gladia-neutral-1000);
+ --md-default-fg-color--light: var(--gladia-neutral-600);
+ --md-default-fg-color--lighter: var(--gladia-neutral-500);
+ --md-default-fg-color--lightest: var(--gladia-neutral-300);
+ --md-default-bg-color: var(--gladia-neutral-0);
+ --md-default-bg-color--light: var(--gladia-neutral-50, #fafafa);
+ --md-default-bg-color--lighter: var(--gladia-neutral-100);
+ --md-default-bg-color--lightest: var(--gladia-neutral-200);
+
+ --md-primary-fg-color: var(--gladia-neutral-0);
+ --md-primary-fg-color--light: var(--gladia-neutral-100);
+ --md-primary-fg-color--dark: var(--gladia-neutral-0);
+ --md-primary-bg-color: var(--gladia-neutral-1000);
+ --md-primary-bg-color--light: var(--gladia-neutral-700);
+
+ --md-accent-fg-color: var(--gladia-purple-400);
+ --md-accent-fg-color--transparent: rgba(148, 122, 252, 0.12);
+ --md-accent-bg-color: var(--gladia-neutral-1000);
+ --md-accent-bg-color--light: var(--gladia-neutral-700);
+
+ --md-code-fg-color: var(--gladia-neutral-800);
+ --md-code-bg-color: var(--gladia-neutral-100);
+ --md-code-hl-color: rgba(148, 122, 252, 0.14);
+
+ --md-typeset-a-color: var(--gladia-neutral-600);
+ --md-typeset-table-color: rgba(0, 0, 0, 0.12);
+ --md-typeset-table-color--light: rgba(0, 0, 0, 0.06);
+
+ --md-footer-bg-color: var(--gladia-neutral-1000);
+ --md-footer-bg-color--dark: var(--gladia-neutral-1000);
+ --md-footer-fg-color: var(--gladia-neutral-400);
+ --md-footer-fg-color--light: var(--gladia-neutral-0);
+ --md-footer-fg-color--lighter: var(--gladia-neutral-500);
+}
+
+/* ── Typography ──────────────────────────────────────────────────────────── */
+
+:root {
+ --md-text-font: var(--gladia-font-sans);
+ --md-code-font: var(--gladia-font-mono);
+}
+
+body {
+ font-weight: 400;
+ letter-spacing: 0;
+ -webkit-font-smoothing: antialiased;
+}
+
+.md-typeset {
+ font-weight: 400;
+ line-height: 1.5;
+}
+
+.md-typeset h1,
+.md-typeset h2,
+.md-typeset h3,
+.md-typeset h4 {
+ font-weight: 400;
+ letter-spacing: -0.02em;
+ color: var(--md-default-fg-color);
+}
+
+.md-typeset h1 {
+ font-size: 2rem;
+ letter-spacing: -0.04em;
+ margin-bottom: 0.6em;
+}
+
+.md-typeset h2 {
+ font-size: 1.4rem;
+ margin-top: 1.8em;
+}
+
+.md-typeset strong {
+ font-weight: 400;
+ color: var(--md-default-fg-color);
+}
+
+/* Eyebrow / section label — Geist Mono, the only brand mono usage outside code */
+.md-typeset .gladia-eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-family: var(--gladia-font-mono);
+ font-size: 0.75rem;
+ font-weight: 400;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--md-default-fg-color--light);
+ margin: 0 0 1rem;
+}
+
+.md-typeset .gladia-eyebrow::before {
+ content: "";
+ width: 6px;
+ height: 6px;
+ border-radius: 9999px;
+ background: var(--gladia-purple-400);
+ flex-shrink: 0;
+}
+
+/* ── Header / nav — glass on black ───────────────────────────────────────── */
+
+.md-header {
+ background-color: rgba(12, 12, 12, 0.72);
+ backdrop-filter: blur(40px);
+ -webkit-backdrop-filter: blur(40px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: none;
+ color: var(--gladia-neutral-0);
+}
+
+[data-md-color-scheme="default"] .md-header {
+ background-color: rgba(255, 255, 255, 0.82);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+ color: var(--gladia-neutral-1000);
+}
+
+/* Product title beside the Gladia wordmark — Material defaults this to 700 */
+.md-header__title {
+ font-family: var(--gladia-font-sans);
+ font-size: 0.95rem;
+ font-weight: 400;
+ letter-spacing: -0.02em;
+}
+
+.md-header__topic,
+.md-header__topic:first-child {
+ font-family: var(--gladia-font-sans);
+ font-weight: 400;
+ letter-spacing: -0.02em;
+}
+
+.md-header__button.md-logo {
+ padding: 0.3rem;
+ margin: 0.1rem 0.35rem 0.1rem 0;
+ color: inherit;
+}
+
+.md-header__button.md-logo .md-logo__svg,
+.md-header__button.md-logo svg {
+ display: block;
+ height: 1.35rem;
+ width: auto;
+}
+
+/* Wordmark uses currentColor — white on dark header, black on light */
+[data-md-color-scheme="slate"] .md-header,
+[data-md-color-scheme="slate"] .md-header__button {
+ color: var(--gladia-neutral-0);
+}
+
+[data-md-color-scheme="default"] .md-header,
+[data-md-color-scheme="default"] .md-header__button {
+ color: var(--gladia-neutral-1000);
+}
+
+.md-tabs {
+ background-color: transparent;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+[data-md-color-scheme="default"] .md-tabs {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
+}
+
+.md-nav__title,
+.md-nav__link--active,
+.md-nav__item .md-nav__link--active,
+.md-nav__link:focus,
+.md-nav__link:hover {
+ color: var(--md-default-fg-color);
+}
+
+.md-nav__link--active {
+ font-weight: 400;
+}
+
+.md-nav__link--active:not(.md-nav__link--passed),
+.md-nav__item .md-nav__link--active {
+ color: var(--gladia-purple-400);
+}
+
+/* Sidebar titles: same sans as site title, all caps */
+.md-nav__title,
+.md-nav__item--section > .md-nav__link {
+ font-family: var(--gladia-font-sans);
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: var(--md-default-fg-color);
+}
+
+/* ── Links — white/grey contrast; purple only as hover spike ─────────────── */
+
+.md-typeset a {
+ color: var(--md-typeset-a-color);
+ text-decoration: underline;
+ text-decoration-color: rgba(148, 122, 252, 0);
+ text-underline-offset: 0.18em;
+ transition:
+ color var(--gladia-duration) var(--gladia-ease-out),
+ text-decoration-color var(--gladia-duration) var(--gladia-ease-out);
+}
+
+.md-typeset a:hover,
+.md-typeset a:focus {
+ color: var(--gladia-purple-400);
+ text-decoration-color: var(--gladia-purple-400);
+}
+
+/* ── Code ────────────────────────────────────────────────────────────────── */
+
+.md-typeset code,
+.md-typeset kbd,
+.md-typeset pre {
+ font-family: var(--gladia-font-mono);
+ font-weight: 400;
+ border-radius: var(--gladia-radius-1);
+}
+
+.md-typeset pre > code {
+ border-radius: var(--gladia-radius-2);
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ padding: var(--gladia-space-4);
+}
+
+[data-md-color-scheme="default"] .md-typeset pre > code {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.md-typeset .tabbed-set > .tabbed-content {
+ border-radius: 0 0 var(--gladia-radius-2) var(--gladia-radius-2);
+}
+
+.md-typeset .tabbed-labels > label {
+ font-weight: 400;
+ border-radius: var(--gladia-radius-5);
+}
+
+/* ── Tables / cards ──────────────────────────────────────────────────────── */
+
+.md-typeset table:not([class]) {
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: var(--gladia-radius-2);
+ overflow: hidden;
+ box-shadow: none;
+}
+
+[data-md-color-scheme="default"] .md-typeset table:not([class]) {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.md-typeset table:not([class]) th {
+ background-color: var(--gladia-neutral-900);
+ font-weight: 400;
+ color: var(--md-default-fg-color--light);
+ font-family: var(--gladia-font-mono);
+ font-size: 0.75rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+[data-md-color-scheme="default"] .md-typeset table:not([class]) th {
+ background-color: var(--gladia-neutral-100);
+}
+
+.md-typeset table:not([class]) td,
+.md-typeset table:not([class]) th {
+ border-color: rgba(255, 255, 255, 0.08);
+ padding: 0.7em 1em;
+}
+
+[data-md-color-scheme="default"] .md-typeset table:not([class]) td,
+[data-md-color-scheme="default"] .md-typeset table:not([class]) th {
+ border-color: rgba(0, 0, 0, 0.08);
+}
+
+/* ── Buttons / search ────────────────────────────────────────────────────── */
+
+.md-search__form {
+ background-color: rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: var(--gladia-radius-5);
+ box-shadow: none;
+}
+
+[data-md-color-scheme="default"] .md-search__form {
+ background-color: var(--gladia-neutral-100);
+ border-color: rgba(0, 0, 0, 0.08);
+}
+
+.md-search__input {
+ font-weight: 400;
+}
+
+.md-typeset .md-button {
+ border-radius: var(--gladia-radius-5);
+ font-weight: 400;
+ letter-spacing: 0;
+ padding: 0.5em 1.25em;
+ transition: transform var(--gladia-duration) var(--gladia-ease-out);
+ border: 1px solid rgba(255, 255, 255, 0.12);
+}
+
+.md-typeset .md-button--primary {
+ background-color: var(--gladia-neutral-0);
+ color: var(--gladia-neutral-1000);
+ border: none;
+}
+
+[data-md-color-scheme="default"] .md-typeset .md-button--primary {
+ background-color: var(--gladia-neutral-1000);
+ color: var(--gladia-neutral-0);
+}
+
+.md-typeset .md-button:hover,
+.md-typeset .md-button:focus {
+ transform: scale(1.02);
+ background-color: var(--gladia-neutral-0);
+ color: var(--gladia-neutral-1000);
+ border-color: transparent;
+}
+
+[data-md-color-scheme="default"] .md-typeset .md-button:hover,
+[data-md-color-scheme="default"] .md-typeset .md-button:focus {
+ background-color: var(--gladia-neutral-1000);
+ color: var(--gladia-neutral-0);
+}
+
+/* Never purple-fill a button (G-11) */
+.md-typeset .md-button--primary:hover,
+.md-typeset .md-button--primary:focus {
+ background-color: var(--gladia-neutral-200);
+ color: var(--gladia-neutral-1000);
+}
+
+[data-md-color-scheme="default"] .md-typeset .md-button--primary:hover,
+[data-md-color-scheme="default"] .md-typeset .md-button--primary:focus {
+ background-color: var(--gladia-neutral-700);
+ color: var(--gladia-neutral-0);
+}
+
+/* ── Sidebar / content chrome ────────────────────────────────────────────── */
+
+.md-sidebar__scrollwrap {
+ scrollbar-color: rgba(255, 255, 255, 0.14) transparent;
+}
+
+.md-typeset hr {
+ border-color: rgba(255, 255, 255, 0.12);
+}
+
+[data-md-color-scheme="default"] .md-typeset hr {
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.md-footer {
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.md-annotation__index {
+ background-color: var(--gladia-purple-400);
+}
+
+/* Focus ring — blue, not purple */
+:focus-visible {
+ outline: 2px solid var(--gladia-blue-400);
+ outline-offset: 2px;
+}
+
+/* Home hero breathing room */
+.md-content article > .gladia-eyebrow:first-child {
+ margin-top: 0.25rem;
+}
+
+.md-typeset .gladia-lead {
+ font-size: 1.125rem;
+ line-height: 1.5;
+ color: var(--md-default-fg-color--light);
+ max-width: 40rem;
+ margin: 0 0 1.75rem;
+}
+
+/* Home hero — center table + CTAs */
+.md-typeset .gladia-hero-center {
+ text-align: center;
+ margin: 0 0 2rem;
+}
+
+.md-typeset .gladia-hero-center .md-typeset__table {
+ display: inline-block;
+ max-width: 100%;
+ margin: 0 auto 1.25rem;
+ text-align: left;
+}
+
+.md-typeset .gladia-hero-center > pre {
+ text-align: left;
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 42rem;
+}
+
+.md-typeset .gladia-hero-center .md-button {
+ margin: 0.35rem 0.35rem 0;
+}
diff --git a/mkdocs.yml b/mkdocs.yml
index 7a483e0..2a74210 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,4 +1,4 @@
-site_name: gladia-normalization
+site_name: Normalization
site_description: Normalize speech-to-text transcripts for fair WER comparison
site_url: https://gladiaio.github.io/normalization/
repo_url: https://github.com/gladiaio/normalization
@@ -6,24 +6,27 @@ repo_name: gladiaio/normalization
edit_uri: edit/main/docs/
docs_dir: docs
site_dir: site
+copyright: Copyright © Gladia
theme:
name: material
+ custom_dir: overrides
+ favicon: assets/favicon.svg
+ font: false
palette:
- - media: "(prefers-color-scheme: light)"
- scheme: default
- primary: blue grey
- accent: teal
+ # Dark-first (Gladia canvas)
+ - scheme: slate
+ primary: custom
+ accent: custom
toggle:
- icon: material/brightness-7
- name: Switch to dark mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: blue grey
- accent: teal
- toggle:
- icon: material/brightness-4
+ icon: material/white-balance-sunny
name: Switch to light mode
+ - scheme: default
+ primary: custom
+ accent: custom
+ toggle:
+ icon: material/weather-night
+ name: Switch to dark mode
features:
- content.code.copy
- content.tabs.link
@@ -37,6 +40,9 @@ theme:
icon:
repo: fontawesome/brands/github
+extra_css:
+ - stylesheets/gladia.css
+
plugins:
- search
@@ -49,6 +55,7 @@ validation:
markdown_extensions:
- admonition
- attr_list
+ - md_in_html
- pymdownx.details
- pymdownx.highlight:
anchor_linenums: true
diff --git a/overrides/main.html b/overrides/main.html
new file mode 100644
index 0000000..94d9808
--- /dev/null
+++ b/overrides/main.html
@@ -0,0 +1 @@
+{% extends "base.html" %}
diff --git a/overrides/partials/logo.html b/overrides/partials/logo.html
new file mode 100644
index 0000000..0a363a5
--- /dev/null
+++ b/overrides/partials/logo.html
@@ -0,0 +1,5 @@
+{# Inlined wordmark so fill="currentColor" follows header text color. #}
+
From 88030bf7f4acca413f0eceff48fd4d69c45426de Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 10:34:02 -0400
Subject: [PATCH 03/10] chore: adjust GitHub Actions permissions for deployment
---
.github/workflows/docs.yml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 847ac68..eb4e81c 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -16,8 +16,6 @@ on:
permissions:
contents: read
- pages: write
- id-token: write
concurrency:
group: pages
@@ -49,6 +47,9 @@ jobs:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
From 763462300eedb7b085a89bf3996bce111a3101e9 Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 11:19:17 -0400
Subject: [PATCH 04/10] fix(doc): fixed secondary button style
---
docs/stylesheets/gladia.css | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/stylesheets/gladia.css b/docs/stylesheets/gladia.css
index 858f668..45f9760 100644
--- a/docs/stylesheets/gladia.css
+++ b/docs/stylesheets/gladia.css
@@ -387,9 +387,18 @@ body {
letter-spacing: 0;
padding: 0.5em 1.25em;
transition: transform var(--gladia-duration) var(--gladia-ease-out);
+ /* Material defaults color to --md-primary-fg-color (black in dark / white in
+ light), which makes outline buttons invisible against the page bg. */
+ color: var(--gladia-neutral-0);
+ background-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.12);
}
+[data-md-color-scheme="default"] .md-typeset .md-button {
+ color: var(--gladia-neutral-1000);
+ border-color: rgba(0, 0, 0, 0.12);
+}
+
.md-typeset .md-button--primary {
background-color: var(--gladia-neutral-0);
color: var(--gladia-neutral-1000);
From 595acae0a18f5b2bb6fb76afe811372ccf7a3bfd Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 11:22:07 -0400
Subject: [PATCH 05/10] docs: update pipeline stage representation for clarity
---
docs/concepts.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/concepts.md b/docs/concepts.md
index 107f676..c75ceba 100644
--- a/docs/concepts.md
+++ b/docs/concepts.md
@@ -3,7 +3,7 @@
Every pipeline runs **three stages**, always in this order:
```text
-text ──► [1] text_pre ──► split ──► [2] word ──► join ──► [3] text_post ──► text
+text > [1] text_pre > split > [2] word > join > [3] text_post > text
```
| Stage | Operates on | Typical work |
From 6ff6d60c1d62090c7f3c68e1a9eb38536a46d00c Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 11:25:30 -0400
Subject: [PATCH 06/10] docs: reorganize navigation structure in MkDocs
configuration for improved clarity
---
mkdocs.yml | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/mkdocs.yml b/mkdocs.yml
index 2a74210..6721312 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -73,9 +73,10 @@ nav:
- Usage:
- Python API: usage/python.md
- CLI: usage/cli.md
- - Presets: presets.md
- - Languages: languages.md
- - Step reference: reference/steps.md
+ - Reference:
+ - Presets: presets.md
+ - Languages: languages.md
+ - Steps: reference/steps.md
- Contributing:
- Overview: contributing/index.md
- Contributor guide: contributing/guide.md
From 924a6c73bb92446a4aa75eeb7ca512e9afa27629 Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 12:00:09 -0400
Subject: [PATCH 07/10] docs: enhance contributing guide formatting
---
docs/contributing/guide.md | 95 ++++++++++++++++++-------------------
docs/stylesheets/gladia.css | 2 +-
2 files changed, 48 insertions(+), 49 deletions(-)
diff --git a/docs/contributing/guide.md b/docs/contributing/guide.md
index 596d705..d08e8d7 100644
--- a/docs/contributing/guide.md
+++ b/docs/contributing/guide.md
@@ -6,56 +6,56 @@ Detailed reference for contributors. Read this before adding a step or language.
Every pipeline runs exactly **three stages**, always in this order:
-1. **Text pre-processing** — full-text transforms before word splitting (placeholder protection, symbol conversion, contraction expansion, …)
-2. **Word processing** — per-token transforms after splitting on spaces (replacements, filler removal, …)
-3. **Text post-processing** — full-text cleanup after rejoining words (placeholder restoration, digit collapsing, …)
+1. **Text pre-processing** : full-text transforms before word splitting (placeholder protection, symbol conversion, contraction expansion, …)
+2. **Word processing** : per-token transforms after splitting on spaces (replacements, filler removal, …)
+3. **Text post-processing** : full-text cleanup after rejoining words (placeholder restoration, digit collapsing, …)
-This ordering is a hard constraint — some steps depend on earlier steps having run. See [How it works](../concepts.md) for more detail.
+This ordering is a hard constraint - some steps depend on earlier steps having run. See [How it works](../concepts.md) for more detail.
---
-## Adding a new language — checklist
+## Adding a new language
-- [ ] Create `languages/{lang}/` with `operators.py`, `replacements.py`, `__init__.py`
-- [ ] Put all word-level substitutions in `replacements.py`; do not add inline entries in `operators.py`
-- [ ] Instantiate a `LanguageConfig` in `operators.py`, filling in all required fields and any optional dict fields your language needs (`time_words`, `sentence_replacements`, etc.)
-- [ ] Subclass `LanguageOperators`, overriding only methods where the _algorithm_ differs (not just the data)
-- [ ] If the language has digit words, populate `digit_words` in `LanguageConfig`
-- [ ] If the language uses spoken time patterns, populate `time_words` with all needed word→digit mappings; if it also uses compound minute expressions (e.g. "twenty-one"), override `get_compound_minutes()` — do **not** put this in config
-- [ ] If number expansion is needed and the algorithm is complex, implement it in a `number_normalizer.py` file and override `expand_written_numbers`; otherwise do not create the file
-- [ ] Decorate the class with `@register_language`
-- [ ] Add one import to `languages/__init__.py`
-- [ ] Add tests in `tests/unit/languages/`
-- [ ] Add a CSV file `tests/e2e/files/{preset}/{language_code}.csv` for each relevant preset (e.g. `tests/e2e/files/gladia-3/fr.csv`)
+- Create `languages/{lang}/` with `operators.py`, `replacements.py`, `__init__.py`
+- Put all word-level substitutions in `replacements.py`; do not add inline entries in `operators.py`
+- Instantiate a `LanguageConfig` in `operators.py`, filling in all required fields and any optional dict fields your language needs (`time_words`, `sentence_replacements`, etc.)
+- Subclass `LanguageOperators`, overriding only methods where the _algorithm_ differs (not just the data)
+- If the language has digit words, populate `digit_words` in `LanguageConfig`
+- If the language uses spoken time patterns, populate `time_words` with all needed word-to-digit mappings; if it also uses compound minute expressions (e.g. "twenty-one"), override `get_compound_minutes()` - do **not** put this in config
+- If number expansion is needed and the algorithm is complex, implement it in a `number_normalizer.py` file and override `expand_written_numbers`; otherwise do not create the file
+- Decorate the class with `@register_language`
+- Add one import to `languages/__init__.py`
+- Add tests in `tests/unit/languages/`
+- Add a CSV file `tests/e2e/files/{preset}/{language_code}.csv` for each relevant preset (e.g. `tests/e2e/files/gladia-3/fr.csv`)
### Language data vs. language behavior
This is the central design rule. Ask: "does the _logic_ change by language, or just the _values_?"
-**`LanguageConfig` (data)** — everything that can be expressed as a value: strings, lists, dicts. Separator characters, currency words, filler words, digit words, number words, time words, sentence replacements. Optional fields default to `None`; steps that read them skip gracefully when `None`.
+**`LanguageConfig` (data)** : everything that can be expressed as a value: strings, lists, dicts. Separator characters, currency words, filler words, digit words, number words, time words, sentence replacements. Optional fields default to `None`; steps that read them skip gracefully when `None`.
-**`LanguageOperators` (behavior)** — only methods where the _algorithm itself_ varies by language. Examples: `expand_contractions`, `expand_written_numbers`, `normalize_numeric_time_formats`, `get_compound_minutes`. If the algorithm is generic and only the _data_ differs, put the data in `LanguageConfig` and the algorithm in the step — not in the operator.
+**`LanguageOperators` (behavior)** : only methods where the _algorithm itself_ varies by language. Examples: `expand_contractions`, `expand_written_numbers`, `normalize_numeric_time_formats`, `get_compound_minutes`. If the algorithm is generic and only the _data_ differs, put the data in `LanguageConfig` and the algorithm in the step - not in the operator.
---
-## Adding a new step — checklist
+## Adding a new step
-- [ ] Add the class to the appropriate file in `steps/text/` or `steps/word/`
-- [ ] Set a unique `name` class attribute
-- [ ] Decorate with `@register_step`
-- [ ] Add one import to `steps/text/__init__.py` or `steps/word/__init__.py`
-- [ ] Place the algorithm in `__call__`; read language data from `operators.config.*`; call operator methods only for genuinely behavioral differences
-- [ ] If the step reads an optional `LanguageConfig` field, guard with `if operators.config.field is None: return text`
-- [ ] Add unit tests in `tests/unit/steps/`
-- [ ] If it involves placeholder protection, add both protect and restore to `steps/text/placeholders.py` and update `pipeline/base.py`'s `validate()` accordingly
-- [ ] Add the step name to relevant preset YAMLs if needed (new preset version if existing presets are affected)
-- [ ] If you added or changed the class docstring, run `uv run scripts/generate_step_docs.py` to regenerate `docs/reference/steps.md`
+- Add the class to the appropriate file in `steps/text/` or `steps/word/`
+- Set a unique `name` class attribute
+- Decorate with `@register_step`
+- Add one import to `steps/text/__init__.py` or `steps/word/__init__.py`
+- Place the algorithm in `__call__`; read language data from `operators.config.*`; call operator methods only for genuinely behavioral differences
+- If the step reads an optional `LanguageConfig` field, guard with `if operators.config.field is None: return text`
+- Add unit tests in `tests/unit/steps/`
+- If it involves placeholder protection, add both protect and restore to `steps/text/placeholders.py` and update `pipeline/base.py`'s `validate()` accordingly
+- Add the step name to relevant preset YAMLs if needed (new preset version if existing presets are affected)
+- If you added or changed the class docstring, run `uv run scripts/generate_step_docs.py` to regenerate `docs/reference/steps.md`
### Choosing a base class
Pick the narrowest one that fits your step.
-**`WordStep`** — use when your transformation operates on a single token in isolation, with no knowledge of neighboring words. This is the only base class for Stage 2 steps.
+**`WordStep`** : use when your transformation operates on a single token in isolation, with no knowledge of neighboring words. This is the only base class for Stage 2 steps.
```python
@register_step
@@ -66,7 +66,7 @@ class MyWordStep(WordStep):
...
```
-**`TextStep`** — the general-purpose base for Stage 1 and Stage 3. Use it when your transformation needs to see the full string, or when none of the more specific bases below fit.
+**`TextStep`** : the general-purpose base for Stage 1 and Stage 3. Use it when your transformation needs to see the full string, or when none of the more specific bases below fit.
```python
@register_step
@@ -77,7 +77,7 @@ class MyTextStep(TextStep):
...
```
-**`ProtectStep`** — a specialization of `TextStep` for replacing a character with a placeholder token. Implement `_pattern`, which returns a compiled regex with **exactly two capture groups** (what comes before and after the character being replaced). The `__call__` is fixed: it applies the pattern as `\1{placeholder}\2`.
+**`ProtectStep`** : a specialization of `TextStep` for replacing a character with a placeholder token. Implement `_pattern`, which returns a compiled regex with **exactly two capture groups** (what comes before and after the character being replaced). The `__call__` is fixed: it applies the pattern as `\1{placeholder}\2`.
```python
@register_step
@@ -89,11 +89,11 @@ class MyProtectStep(ProtectStep):
return re.compile(r"(\d+)X(\d+)") # two capture groups required
```
-Use `ProtectStep` when: one regex pattern maps to exactly one placeholder substitution.
+> Use `ProtectStep` when: one regex pattern maps to exactly one placeholder substitution.
-Use `TextStep` directly instead when: a single pass must protect two different symbols, the replacement needs to absorb surrounding whitespace with `\s*`, or the replacement is a per-match function rather than a fixed template.
+> Use `TextStep` directly instead when: a single pass must protect two different symbols, the replacement needs to absorb surrounding whitespace with `\s*`, or the replacement is a per-match function rather than a fixed template.
-**`RestoreStep`** — a specialization of `TextStep` for restoring a placeholder back to a string. Implement `_replacement`, which returns the string to substitute in. The `__call__` does a plain `str.replace` of the placeholder (and its case-folded form).
+**`RestoreStep`** : a specialization of `TextStep` for restoring a placeholder back to a string. Implement `_replacement`, which returns the string to substitute in. The `__call__` does a plain `str.replace` of the placeholder (and its case-folded form).
```python
@register_step
@@ -105,9 +105,9 @@ class MyRestoreStep(RestoreStep):
return operators.config.some_word or " "
```
-Use `RestoreStep` when: restoration is a straight token swap with no surrounding whitespace to absorb and no additional logic needed.
+> Use `RestoreStep` when: restoration is a straight token swap with no surrounding whitespace to absorb and no additional logic needed.
-Use `TextStep` directly instead when: the placeholder was inserted with spaces around it (requiring `re.sub` with `\s*` to avoid double spaces), the marker should be deleted entirely rather than replaced, or post-replacement cleanup is needed.
+> Use `TextStep` directly instead when: the placeholder was inserted with spaces around it (requiring `re.sub` with `\s*` to avoid double spaces), the marker should be deleted entirely rather than replaced, or post-replacement cleanup is needed.
---
@@ -119,9 +119,9 @@ Unit tests live under `tests/unit/steps/text/` or `tests/unit/steps/word/`, mirr
The `tests/unit/steps/text/conftest.py` provides two fixtures and a helper:
-- `operators` — a bare `LanguageOperators()` instance (language-agnostic)
-- `english_operators` — an `EnglishOperators()` instance
-- `assert_text_step_registered(step_cls)` — verifies the step is in the registry under its name
+- `operators` : a bare `LanguageOperators()` instance (language-agnostic)
+- `english_operators` : an `EnglishOperators()` instance
+- `assert_text_step_registered(step_cls)` : verifies the step is in the registry under its name
Every test file for a step should at minimum:
@@ -161,7 +161,7 @@ def test_my_step_with_english(english_operators):
E2E tests validate the full pipeline (preset + language) against CSV fixtures. The test runner lives in `tests/e2e/normalization_test.py` and CSV files are organized under `tests/e2e/files/`.
-**Directory structure** — one folder per preset, one CSV per language:
+**Directory structure** : one folder per preset, one CSV per language:
```
tests/e2e/files/
@@ -178,19 +178,18 @@ tests/e2e/files/
sv.csv
```
-**CSV format** — two columns (`input,expected`), no quoting needed unless the value contains a comma:
+**CSV format** : two columns (`input,expected`), no quoting needed unless the value contains a comma:
```
input,expected
"$1,000,000",1000000 dollars
hello world,hello world
```
+> The language is derived from the filename (e.g. `fr.csv` → language code `fr`). Use `default.csv` for the language-agnostic fallback.
-The language is derived from the filename (e.g. `fr.csv` → language code `fr`). Use `default.csv` for the language-agnostic fallback.
+**Adding test cases for an existing preset** : drop rows into the appropriate `{language_code}.csv` file, or create a new CSV if the language isn't covered yet. Tests are discovered automatically.
-**Adding test cases for an existing preset** — drop rows into the appropriate `{language_code}.csv` file, or create a new CSV if the language isn't covered yet. Tests are discovered automatically.
-
-**Registering a new preset** — add a block to `normalization_test.py` following the existing pattern:
+**Registering a new preset** : add a block to `normalization_test.py` following the existing pattern:
```python
_MY_PRESET_DIR = _FILES_DIR / "my-preset"
@@ -203,7 +202,7 @@ for _language in sorted(_MY_PRESET_BY_LANGUAGE):
)
```
-Pipelines are cached per language to avoid reloading for each parametrized case.
+> Pipelines are cached per language to avoid reloading for each parametrized case.
---
@@ -211,6 +210,6 @@ Pipelines are cached per language to avoid reloading for each parametrized case.
- **Data vs. behavior**: if only the _values_ change by language, put them in `LanguageConfig`. If the _algorithm_ changes, override a method in `LanguageOperators`.
- **Steps are language-agnostic**: a step must not contain any language-specific logic or string literals. Read from `operators.config.*` or call `operators.method()`.
-- **Presets are immutable**: never modify a published preset YAML — new behavior means a new preset file.
+- **Presets are immutable**: never modify a published preset YAML - new behavior means a new preset file.
- **Placeholder pairs**: every `protect_*` step in Stage 1 must have a matching `restore_*` in Stage 3. The pipeline validates this at load time.
- **Language folders are self-contained**: everything specific to a language lives inside its folder. Helpers used only by one language (e.g. `number_normalizer.py`) go in that language's folder, not in `steps/`.
\ No newline at end of file
diff --git a/docs/stylesheets/gladia.css b/docs/stylesheets/gladia.css
index 45f9760..a3e3d91 100644
--- a/docs/stylesheets/gladia.css
+++ b/docs/stylesheets/gladia.css
@@ -159,7 +159,7 @@ body {
}
.md-typeset strong {
- font-weight: 400;
+ font-weight: 600;
color: var(--md-default-fg-color);
}
From 3573b4a660d3083dee760eb5531a27b675b5c26a Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 12:09:35 -0400
Subject: [PATCH 08/10] docs: update installation instructions and enhance
table formatting for clarity
---
docs/getting-started.md | 9 +++++----
docs/index.md | 16 ++++++----------
docs/languages.md | 4 +++-
docs/stylesheets/gladia.css | 16 ++++++++++++++--
4 files changed, 28 insertions(+), 17 deletions(-)
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 28261fc..a96a983 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -2,16 +2,17 @@
## Install
-=== "pip"
+
+=== "uv"
```bash
- pip install gladia-normalization
+ uv add gladia-normalization
```
-=== "uv"
+=== "pip"
```bash
- uv add gladia-normalization
+ pip install gladia-normalization
```
=== "From source"
diff --git a/docs/index.md b/docs/index.md
index ddda0a3..8a48936 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,22 +8,18 @@ Normalize speech-to-text transcripts before computing Word Error Rate, so format
-| Ground truth | STT output | Without normalization |
-| --- | --- | --- |
-| It's $50 | it is fifty dollars | treated as errors |
-| 3:00 PM | 3 pm | treated as errors |
-| Mr. Smith | mister smith | treated as errors |
-
-```text
-Input: "It's $50.9 at 3:00PM — y'know, roughly."
-Output: "it is 50 point 9 dollars at 3 pm you know roughly"
-```
+| Ground truth | STT output | WER |
+| ------------ | ------------------- | ---- |
+| It's $50 | it is fifty dollars | 100% |
+| 3:00 PM | 3 pm | 100% |
+| Mr. Smith | mister smith | 100% |
[Get started](getting-started.md){ .md-button .md-button--primary }
[How it works](concepts.md){ .md-button }
+
## Quick example
```python
diff --git a/docs/languages.md b/docs/languages.md
index eff3331..dd23461 100644
--- a/docs/languages.md
+++ b/docs/languages.md
@@ -2,6 +2,7 @@
Pass a language code to `load_pipeline` or `--language`. Unknown codes fall back to a language-agnostic default (independent transforms only).
+
| Code | Language |
| --- | --- |
| `da` | Danish |
@@ -14,8 +15,9 @@ Pass a language code to `load_pipeline` or `--language`. Unknown codes fall back
| `nl` | Dutch |
| `no` | Norwegian |
| `sv` | Swedish |
+
-## What “language-aware” means
+## What "language-aware" means
Each language folder under `normalization/languages/` provides:
diff --git a/docs/stylesheets/gladia.css b/docs/stylesheets/gladia.css
index a3e3d91..092c235 100644
--- a/docs/stylesheets/gladia.css
+++ b/docs/stylesheets/gladia.css
@@ -354,13 +354,25 @@ body {
.md-typeset table:not([class]) td,
.md-typeset table:not([class]) th {
- border-color: rgba(255, 255, 255, 0.08);
+ border: none;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ border-right: 1px solid rgba(255, 255, 255, 0.08);
padding: 0.7em 1em;
}
+.md-typeset table:not([class]) td:last-child,
+.md-typeset table:not([class]) th:last-child {
+ border-right: none;
+}
+
+.md-typeset table:not([class]) tr:last-child td {
+ border-bottom: none;
+}
+
[data-md-color-scheme="default"] .md-typeset table:not([class]) td,
[data-md-color-scheme="default"] .md-typeset table:not([class]) th {
- border-color: rgba(0, 0, 0, 0.08);
+ border-bottom-color: rgba(0, 0, 0, 0.08);
+ border-right-color: rgba(0, 0, 0, 0.08);
}
/* ── Buttons / search ────────────────────────────────────────────────────── */
From 63dc270eb3a44e1b29a14c13a0f96220b1bf7d65 Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 12:12:14 -0400
Subject: [PATCH 09/10] docs: refine contributing guide index
---
docs/contributing/index.md | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/docs/contributing/index.md b/docs/contributing/index.md
index 16d3695..f4a50c8 100644
--- a/docs/contributing/index.md
+++ b/docs/contributing/index.md
@@ -1,6 +1,6 @@
# Contributing
-Bug reports, new steps, and new language support are welcome.
+Bug reports, new steps, and new languages are welcome. Setup and where to start are below. Check [Contributor guide](guide.md) for design rules.
## Setup
@@ -30,13 +30,11 @@ uv run mkdocs build
## How to help
-| Contribution | Start here |
-| --- | --- |
-| New language | [Checklist](guide.md#adding-a-new-language-checklist) |
-| New step | [Checklist](guide.md#adding-a-new-step-checklist) |
-| Bug report | GitHub issue with reproduce steps + expected vs actual |
-| Question | GitHub issue with the `question` label |
+| Contribution | Start here |
+| ------------ | ------------------------------------------------------ |
+| New language | [Checklist](guide.md#adding-a-new-language-checklist) |
+| New step | [Checklist](guide.md#adding-a-new-step-checklist) |
+| Bug report | GitHub issue with reproduce steps + expected vs actual |
+| Question | GitHub issue with the `question` label |
-Full design rules, base-class choice, and test conventions: [Contributor guide](guide.md).
-
-Also see [`CONTRIBUTING.md`](https://github.com/gladiaio/normalization/blob/main/CONTRIBUTING.md) in the repo root for PR workflow and commit style.
+Also see [`CONTRIBUTING.md`](https://github.com/gladiaio/normalization/blob/main/CONTRIBUTING.md) for PR workflow and commit style.
From 3467213afd51ce8b4d3ae25d98c1077f82cbd7a9 Mon Sep 17 00:00:00 2001
From: karamouche
Date: Tue, 14 Jul 2026 12:16:54 -0400
Subject: [PATCH 10/10] docs: fixed anchor links with title rewriting
---
docs/contributing/index.md | 4 ++--
docs/languages.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/contributing/index.md b/docs/contributing/index.md
index f4a50c8..d099f73 100644
--- a/docs/contributing/index.md
+++ b/docs/contributing/index.md
@@ -32,8 +32,8 @@ uv run mkdocs build
| Contribution | Start here |
| ------------ | ------------------------------------------------------ |
-| New language | [Checklist](guide.md#adding-a-new-language-checklist) |
-| New step | [Checklist](guide.md#adding-a-new-step-checklist) |
+| New language | [Checklist](guide.md#adding-a-new-language) |
+| New step | [Checklist](guide.md#adding-a-new-step) |
| Bug report | GitHub issue with reproduce steps + expected vs actual |
| Question | GitHub issue with the `question` label |
diff --git a/docs/languages.md b/docs/languages.md
index dd23461..d3c8fd8 100644
--- a/docs/languages.md
+++ b/docs/languages.md
@@ -29,4 +29,4 @@ Steps stay language-agnostic: they read `operators.config.*` or call operator me
## Adding a language
-See the [contributor guide](contributing/guide.md#adding-a-new-language-checklist).
+See the [contributor guide](contributing/guide.md#adding-a-new-language).