-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
1084 lines (1004 loc) · 61 KB
/
Copy pathindex.html
File metadata and controls
1084 lines (1004 loc) · 61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Michael Chen, AI software engineer</title>
<meta name="description" content="Michael Chen builds software for people he knows, including himself. A distilled TTS engine that runs 53x faster and still misses its own quality bar, patches in other people's repositories, and a shelf of apps his friends actually use.">
<meta name="author" content="Michael Chen">
<link rel="canonical" href="https://mchen04.github.io/">
<meta name="theme-color" content="#e8eae3" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#101417" media="(prefers-color-scheme: dark)">
<meta property="og:type" content="website">
<meta property="og:url" content="https://mchen04.github.io/">
<meta property="og:title" content="Michael Chen, AI software engineer">
<meta property="og:description" content="I build software for people I know, including me. Measured systems, shipped products, and the numbers that did not clear the bar.">
<meta property="og:image" content="https://mchen04.github.io/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Michael Chen, AI software engineer">
<meta name="twitter:description" content="I build software for people I know, including me.">
<meta name="twitter:image" content="https://mchen04.github.io/og.png">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="icon" href="favicon.ico" sizes="32x32">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<link rel="preload" href="fonts/bricolage.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="fonts/newsreader.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="fonts/plexmono.woff2" as="font" type="font/woff2" crossorigin>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Person","name":"Michael Chen",
"jobTitle":"AI Software Engineer","url":"https://mchen04.github.io/",
"email":"mailto:michaelluochen1@gmail.com",
"address":{"@type":"PostalAddress","addressRegion":"CA","addressCountry":"US","addressLocality":"Bay Area"},
"alumniOf":{"@type":"CollegeOrUniversity","name":"University of California, Riverside"},
"worksFor":{"@type":"Organization","name":"Epoch AI / Zerg AI"},
"sameAs":["https://github.com/mchen04","https://www.linkedin.com/in/michael-luo-chen/","https://huggingface.co/mchen04"]}
</script>
<style>
/* ============================================================================
INSTRUMENT — a measurement readout.
Palette is plotter paper, not cream. Accent encodes measurement state:
--trace marks a reading that held, --flag marks one that did not.
Nothing is colored for decoration.
========================================================================= */
@font-face{font-family:'Bricolage';src:url(fonts/bricolage.woff2) format('woff2');
font-weight:400 800;font-stretch:75% 100%;font-display:swap;
unicode-range:U+0000-00FF,U+2000-206F,U+2190-21FF,U+2200-22FF}
@font-face{font-family:'Newsreader';src:url(fonts/newsreader.woff2) format('woff2');
font-weight:300 600;font-display:swap;
unicode-range:U+0000-00FF,U+2000-206F,U+2190-21FF}
@font-face{font-family:'PlexMono';src:url(fonts/plexmono.woff2) format('woff2');
font-weight:400;font-display:swap;
unicode-range:U+0000-00FF,U+2000-206F,U+2190-21FF}
:root{
color-scheme:light dark;
/* --- surface ---------------------------------------------------------- */
--paper:#e8eae3; /* cool plotter stock, deliberately not cream */
--paper-lift:#f1f2ec;
--ink:#14181c; /* blue-black, never #000 */
--ink-2:#3b4348;
--mute:#5d6763;
--rule:#c6cbc0;
--rule-soft:#d6dacf;
/* --- measurement state ------------------------------------------------ */
--trace:#08636b; /* a reading that held */
--flag:#a8102a; /* a reading that missed its bar */
--trace-wash:#08636b1a;
--flag-wash:#a8102a14;
/* --- type ------------------------------------------------------------- */
--display:'Bricolage','Trebuchet MS',system-ui,sans-serif;
--body:'Newsreader',Georgia,'Times New Roman',serif;
--data:'PlexMono',ui-monospace,SFMono-Regular,Menlo,monospace;
/* fluid scale, 8pt-derived */
--t-mega:clamp(2.9rem,7.6vw,7rem);
--t-xl:clamp(2.1rem,4.6vw,4rem);
--t-lg:clamp(1.6rem,2.6vw,2.4rem);
--t-md:clamp(1.15rem,1.5vw,1.4rem);
--t-body:1.0625rem;
--t-data:.8125rem;
--t-micro:.6875rem;
/* --- rhythm (intentionally uneven per section, see each block) --------- */
--gut:clamp(1.25rem,4.2vw,3.5rem);
--measure:64ch;
--tap:44px; /* minimum comfortable touch target */
--head:53px; /* masthead height; JS re-syncs it after font load */
--ease:cubic-bezier(.22,.61,.36,1);
}
[data-theme="scope"]{
--paper:#101417;
--paper-lift:#171c20;
--ink:#e4e8e2;
--ink-2:#b3bcb7;
--mute:#8b958f;
--rule:#2a3237;
--rule-soft:#20272b;
--trace:#7fb8bc;
--flag:#e08494;
--trace-wash:#7fb8bc17;
--flag-wash:#e0849414;
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="bench"]){
--paper:#101417;--paper-lift:#171c20;
--ink:#e4e8e2;--ink-2:#b3bcb7;--mute:#8b958f;
--rule:#2a3237;--rule-soft:#20272b;
--trace:#7fb8bc;--flag:#e08494;
--trace-wash:#7fb8bc17;--flag-wash:#e0849414;
}
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{
margin:0;background:var(--paper);color:var(--ink);
font-family:var(--body);font-size:var(--t-body);line-height:1.6;
font-variation-settings:'opsz' 16;
-webkit-font-smoothing:antialiased;
overflow-x:clip;
}
svg{display:block;max-width:100%}
a{color:inherit}
h1,h2,h3{margin:0;font-family:var(--display);font-weight:600;line-height:1.02;
letter-spacing:-.025em;font-variation-settings:'wdth' 90}
p,figure,dl,dd{margin:0}
:focus-visible{outline:2px solid var(--trace);outline-offset:3px;border-radius:2px}
::selection{background:var(--ink);color:var(--paper)}
.shell{max-width:1400px;margin-inline:auto;padding-inline:var(--gut)}
.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:var(--paper);
padding:.75rem 1rem;font-family:var(--data);font-size:var(--t-data);z-index:99}
.skip:focus{left:.5rem;top:.5rem}
/* --- utility label. lowercase on purpose: all-caps eyebrows are a tell --- */
.tag{font-family:var(--data);font-size:var(--t-micro);letter-spacing:.06em;
color:var(--mute);display:flex;flex-wrap:wrap;align-items:baseline;gap:.2rem .6rem;line-height:1.5}
.tag b{color:var(--ink);font-weight:400}
.num{font-family:var(--data);font-variant-numeric:tabular-nums}
/* ============================ masthead =================================== */
.masthead{position:sticky;top:0;z-index:40;background:var(--paper);
border-bottom:1px solid var(--rule-soft)}
.masthead .shell{display:flex;align-items:center;justify-content:space-between;
gap:1rem;height:52px;font-family:var(--data);font-size:var(--t-data)}
.masthead nav{min-width:0}
.masthead nav ul{display:flex;flex-wrap:wrap;gap:.4rem 1.25rem;list-style:none;margin:0;padding:0}
/* Seven sections stopped fitting on a phone. The bar is a fixed 52px, so a
wrapped second row would be clipped; scroll the nav instead of the page. */
@media (max-width:560px){
.masthead nav{overflow-x:auto;scrollbar-width:none}
.masthead nav::-webkit-scrollbar{display:none}
.masthead nav ul{flex-wrap:nowrap}
}
.masthead nav a{text-decoration:none;color:var(--ink-2);white-space:nowrap;
display:flex;align-items:center;min-height:var(--tap);padding:0 .1rem;border-bottom:1px solid transparent;transition:color .18s var(--ease),border-color .18s var(--ease)}
.masthead nav a:hover,.masthead nav a:focus-visible{color:var(--ink);border-bottom-color:var(--trace)}
.masthead nav a[aria-current]{color:var(--ink);border-bottom-color:var(--ink)}
.mark{font-family:var(--display);font-weight:700;letter-spacing:-.03em;font-size:.95rem;
text-decoration:none;font-variation-settings:'wdth' 78;
display:inline-flex;align-items:center;min-height:var(--tap);padding-right:.5rem}
.themer{display:none;font-family:var(--data);font-size:var(--t-micro);color:var(--mute);
background:none;border:0;padding:.55rem 0 .55rem .1rem;min-height:var(--tap);cursor:pointer;
gap:.5rem;transition:color .18s var(--ease)}
.js .themer{display:inline-flex;align-items:center}
.themer::before{content:"";width:7px;height:7px;border-radius:50%;
border:1px solid var(--mute);background:transparent;transition:background .18s var(--ease),border-color .18s var(--ease)}
[data-theme="scope"] .themer::before{background:var(--trace);border-color:var(--trace)}
.themer:hover{color:var(--ink)}
.themer:hover::before{border-color:var(--ink)}
/* ============================ hero ======================================= */
/* Shape: oversized display type left, readings ledger right. No section below
repeats it. */
.hero{padding-top:clamp(2.5rem,8vh,6rem);border-bottom:1px solid var(--rule)}
.hero h1{font-size:var(--t-mega);max-width:15ch;font-weight:700;
font-variation-settings:'wdth' 84;letter-spacing:-.038em;text-wrap:balance}
.hero-role{margin-top:1.7rem;max-width:26ch;font-family:var(--display);font-weight:600;
font-size:clamp(1.6rem,3.2vw,2.75rem);line-height:1.15;letter-spacing:-.025em;
font-variation-settings:'wdth' 86}
.hero-role b{font-weight:600;color:var(--trace)}
.hero-lead{margin-top:1rem;max-width:44ch;font-size:var(--t-md);color:var(--ink-2);
font-variation-settings:'opsz' 24}
/* two columns at desktop: the claim, and the readings that back it */
.hero-body{display:grid;grid-template-columns:minmax(0,1fr);gap:2.5rem}
@media (min-width:1120px){
.hero-body{grid-template-columns:minmax(0,1fr) 19rem;gap:clamp(2rem,5vw,5rem);align-items:start}
}
.readings{border:1px solid var(--rule);background:var(--paper-lift)}
.readings .rhead{font-family:var(--data);font-size:var(--t-micro);font-weight:400;color:var(--mute);
letter-spacing:.06em;padding:.7rem .9rem;border-bottom:1px solid var(--rule);
font-variation-settings:normal}
.readings dl{margin:0}
.readings div{display:grid;grid-template-columns:1fr auto;gap:.15rem .75rem;align-items:baseline;
padding:.7rem .9rem .6rem;border-bottom:1px solid var(--rule-soft)}
.readings div:last-child{border-bottom:0}
.readings dt{font-family:var(--data);font-size:var(--t-micro);color:var(--mute);margin:0}
.readings dd{margin:0;font-family:var(--display);font-weight:600;font-size:1.15rem;
letter-spacing:-.02em;font-variant-numeric:tabular-nums;text-align:right}
.readings .off dt,.readings .off dd{color:var(--flag)}
.hero-strip{margin-top:2.25rem;display:flex;flex-wrap:wrap;
gap:.5rem 2.5rem;font-family:var(--data);font-size:var(--t-data);color:var(--mute)}
.hero-strip b{color:var(--ink);font-weight:400}
.hero-links{margin-top:1.1rem;display:flex;flex-wrap:wrap;gap:.15rem 1.75rem;font-family:var(--data);font-size:var(--t-data)}
.hero-links a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--rule);
padding:.6rem 0 .25rem;min-height:var(--tap);display:inline-flex;align-items:center;transition:border-color .18s var(--ease),color .18s var(--ease)}
.hero-links a:hover,.hero-links a:focus-visible{color:var(--trace);border-bottom-color:var(--trace)}
/* ============================ section frame ============================== */
/* Deliberately different vertical rhythm per shelf. Uniform section padding
is a named signature of generated pages. */
.shelf{border-bottom:1px solid var(--rule);padding-bottom:4rem}
.shelf-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:.75rem 1.5rem;
padding-block:2.25rem 1rem}
/* four sections sit the deck under the heading; tools and about keep it
right-aligned, so the header band is not one repeated formula six times */
.shelf-head.wide{display:block}
.shelf-head.wide .said{margin:.6rem 0 0;text-align:left;
max-width:56ch;font-family:var(--body);font-size:1.05rem;color:var(--ink-2)}
.shelf-head h2{font-size:var(--t-xl);font-weight:700;font-variation-settings:'wdth' 80}
/* the shelves are not equal, so they do not announce themselves equally */
#measured .shelf-head h2{font-size:clamp(2.4rem,5.4vw,4.75rem)}
#friends .shelf-head h2,#tools .shelf-head h2,#about .shelf-head h2{
font-size:clamp(1.7rem,3vw,2.6rem)}
.shelf-head .said{font-family:var(--data);font-size:var(--t-data);color:var(--mute);
max-width:52ch;margin-left:auto;text-align:right}
@media (max-width:820px){
.shelf-head .said,.shelf-head.wide .said{font-family:var(--body);font-size:1rem;color:var(--ink-2)}
.shelf-head .said{margin-left:0;text-align:left}}
/* ---------- 01 measured: asymmetric case studies with real plots --------- */
#measured{padding-bottom:5rem}
.cases{list-style:none;margin:0;padding:0}
.case{padding-block:clamp(2.5rem,6vh,4rem);border-top:1px solid var(--rule-soft);
display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);
gap:clamp(1.5rem,4vw,4rem);align-items:start}
.cases > li:first-child .case{border-top:0}
.case.solo{grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr)}
.case.solo .aside{grid-column:2;grid-row:1}
.case.solo .case-what{max-width:var(--measure)}
@media (max-width:980px){
.case.solo{grid-template-columns:minmax(0,1fr)}
.case.solo .aside{grid-column:1;grid-row:2}
}
.case-title{display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem 1rem}
.case-title h3{font-size:var(--t-lg);font-weight:700}
.case-title a{color:inherit;text-decoration:none;border-bottom:2px solid var(--trace);
transition:color .45s var(--ease)}
.case-title a:hover,.case-title a:focus-visible{color:var(--trace)}
.case-what{margin-top:1rem;max-width:var(--measure);color:var(--ink-2)}
.case-what + .case-what{margin-top:.85rem}
.case-what strong{font-weight:600;color:var(--ink)}
.readout{margin-top:1.5rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));
gap:1px;background:var(--rule);border:1px solid var(--rule)}
.readout div{background:var(--paper);padding:.7rem .8rem;display:flex;flex-direction:column}
@media (max-width:560px){
/* stack as label/value rows so a short final row cannot leave an empty cell */
.readout{grid-template-columns:1fr}
.readout div{display:grid;grid-template-columns:1fr auto;align-items:baseline;gap:.75rem}
.readout dt{min-height:0}
.readout dd{margin-top:0;text-align:right}
}
.readout dt{font-family:var(--data);font-size:var(--t-micro);color:var(--mute);margin:0;
min-height:2.1em;padding-bottom:.15rem}
.readout .u{font-size:.6em}
.readout dd{margin:auto 0 0;font-family:var(--display);font-size:1.4rem;font-weight:600;
letter-spacing:-.02em;font-variant-numeric:tabular-nums}
/* the honest miss. its own visual state, not a footnote. */
.miss{margin-top:1.5rem;border-left:3px solid var(--flag);background:var(--flag-wash);
padding:.9rem 1.1rem}
.miss .tag b{color:var(--flag)}
.miss p{margin-top:.5rem;color:var(--ink-2);max-width:60ch}
.plot{position:sticky;top:calc(var(--head) + 14px);border:1px solid var(--rule);
background:var(--paper-lift);padding:1rem 1.1rem 1.1rem;grid-column:2}
.case > div{grid-column:1;grid-row:1}
.plot figcaption{font-family:var(--data);font-size:var(--t-micro);color:var(--mute);
line-height:1.5;margin-bottom:.9rem}
.plot figcaption b{color:var(--ink);font-weight:400}
.plot svg{width:100%;height:auto;overflow:visible;max-width:520px}
.src{margin-top:1.1rem;font-family:var(--data);font-size:var(--t-micro);color:var(--mute);
line-height:1.9}
.plot .src{margin-top:.85rem;line-height:2}
.src a{color:var(--ink-2)}
.src a:hover,.src a:focus-visible,
.essay p a:hover,.essay p a:focus-visible{color:var(--trace)}
.go,.h{white-space:nowrap}
.gridline{stroke:var(--rule);stroke-width:1;vector-effect:non-scaling-stroke}
.barok{fill:var(--trace)}
.barbad{fill:var(--flag)}
.plotlabel{font-family:var(--data);font-size:7px;fill:var(--mute)}
.plotbar-label{font-family:var(--data);font-size:7.5px;fill:var(--ink)}
/* ---------- 02 shipped: a ledger. rows, not cards. ---------------------- */
.ledger{border-top:1px solid var(--rule);list-style:none;margin:0;padding:0}
.row{display:grid;grid-template-columns:8.5rem minmax(0,1fr);gap:.4rem 2rem;
padding-block:1.5rem;border-bottom:1px solid var(--rule-soft);
transition:background .12s linear}
.ledger > li:hover .row{background:var(--trace-wash)}
@media (min-width:1100px){
.row{grid-template-columns:7.5rem minmax(0,1fr) 11rem;gap:.4rem 1.75rem}
.row .built{grid-column:3;grid-row:1;margin-top:.4rem}
}
.row > .when{font-family:var(--data);font-size:var(--t-data);color:var(--mute);padding-top:.2rem}
.row h3{font-size:1.35rem;font-weight:600}
.row h3 a{color:inherit;text-decoration:none;border-bottom:1px solid var(--rule);
transition:border-color .18s var(--ease),color .18s var(--ease)}
.row h3 a:hover,.row h3 a:focus-visible{color:var(--trace);border-bottom-color:var(--trace)}
.row p{margin-top:.55rem;color:var(--ink-2);max-width:var(--measure)}
.row .built{margin-top:.7rem;font-family:var(--data);font-size:var(--t-micro);color:var(--mute)}
/* ---------- 03 books: narrow prose. widest margins on the page. --------- */
#books{padding-bottom:4.5rem}
.essay{max-width:62ch;padding-top:1rem}
.essay p{margin-top:1.15rem;font-size:1.15rem;line-height:1.65;font-variation-settings:'opsz' 22}
.essay p:first-child{margin-top:0;font-size:1.3rem;line-height:1.5}
#books .three{margin-top:2.25rem;display:grid;gap:1px;background:var(--rule);list-style:none;padding:0;
border:1px solid var(--rule)}
#books .three > li{background:var(--paper);padding:.9rem 1rem;
display:grid;grid-template-columns:1fr auto;grid-template-rows:auto auto;gap:.3rem 1rem;align-items:baseline;
transition:background .22s var(--ease)}
#books .three > li:hover{background:var(--paper-lift)}
#books .three a{text-decoration:none;color:inherit}
.essay p a{transition:color .26s var(--ease)}
/* the books essay runs beside its screenshot, same two-column read as a case. */
.essay-wrap{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);
gap:clamp(1.5rem,4vw,4rem);align-items:start}
.essay-wrap .essay{grid-column:1}
.shot{grid-column:2;position:sticky;top:calc(var(--head) + 14px);margin-top:1rem;
border:1px solid var(--rule);background:var(--paper-lift);padding:1rem 1.1rem 1.1rem}
.shot figcaption{font-family:var(--data);font-size:var(--t-micro);color:var(--mute);
line-height:1.5;margin-bottom:.9rem}
.shot figcaption b{color:var(--ink);font-weight:400}
.shot img{display:block;width:100%;max-width:260px;height:auto;margin-inline:auto;
border:1px solid var(--rule-soft);border-radius:14px}
.shot .src{margin-top:.85rem;line-height:2}
@media (max-width:900px){
.essay-wrap{grid-template-columns:minmax(0,1fr)}
.essay-wrap .essay,.shot{grid-column:1}
.shot{position:static;margin-top:2rem}
}
#books .three h3 a{transition:color .22s var(--ease)}
#books .three h3 a:hover,#books .three h3 a:focus-visible{color:var(--trace)}
#books .three h3 a,.strip h3 a,.row h3 a{display:inline-block;padding-block:.6rem .4rem}
#books .three .n{font-family:var(--display);font-weight:600;font-size:1.05rem;margin:0;
letter-spacing:-.015em;transition:color .22s var(--ease)}
#books .three .d{grid-column:1/-1;font-family:var(--data);font-size:var(--t-micro);color:var(--mute);line-height:1.6}
@media (min-width:900px){
/* the three artefacts run the full measure under the narrative, so the
section reads as essay-then-evidence and leaves no dead column */
#books .three{grid-template-columns:repeat(3,minmax(0,1fr))}
}
#books .three .go{font-family:var(--data);font-size:var(--t-micro);color:var(--trace)}
/* ---------- 04 friends: a directory listing. one line each. ------------- */
.dir{border-top:1px solid var(--rule);font-family:var(--data);font-size:var(--t-data);
list-style:none;margin:0;padding:0}
.dir a{display:grid;grid-template-columns:1.5rem 11.5rem minmax(0,1fr) auto;
gap:.9rem;align-items:baseline;padding:.8rem .25rem;min-height:var(--tap);text-decoration:none;
border-bottom:1px solid var(--rule-soft);transition:background .09s linear,padding-left .34s var(--ease)}
.dir a:hover,.dir a:focus-visible{background:var(--trace-wash);padding-left:.75rem}
.dir .i{color:var(--mute);font-variant-numeric:tabular-nums}
.dir .n{color:var(--ink);font-family:var(--display);font-weight:600;font-size:.95rem;
letter-spacing:-.01em;margin:0;line-height:1.3}
.dir .d{color:var(--mute);line-height:1.5}
.dir .h{color:var(--trace)}
@media (max-width:720px){
.dir a{grid-template-columns:1.5rem minmax(0,1fr) auto;gap:.25rem .8rem}
.dir .n{grid-column:2;grid-row:1}
/* .d spans the full width on row 2, so .h must be placed or it wraps to a
third row and hangs off the left edge */
.dir .h{grid-column:3;grid-row:1}
.dir .d{grid-column:2/-1;grid-row:2}
}
/* ---------- 05 tools: compact strip ------------------------------------- */
.strip{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
gap:1px;background:var(--rule);border:1px solid var(--rule);list-style:none;margin:0;padding:0}
.strip > li{display:flex}
@media (max-width:560px){.strip{grid-template-columns:minmax(0,1fr)}}
@media (min-width:1150px){.strip{grid-template-columns:repeat(4,minmax(0,1fr))}}
/* upstream mixes one long-form entry with short ones: never stretch it to four */
@media (min-width:1150px){#upstream .strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
/* five entries in two columns would otherwise leave a lit empty cell */
@media (min-width:561px){#upstream .strip > li:last-child:nth-child(odd){grid-column:1/-1}}
.strip article{background:var(--paper);padding:1.1rem 1.15rem;display:flex;flex-direction:column;gap:.5rem;flex:1;
transition:background .12s linear;position:relative}
.strip li:hover article{background:var(--paper-lift)}
.strip h3{font-size:1.05rem;font-weight:600}
.strip h3 a{color:inherit;text-decoration:none;border-bottom:1px solid var(--rule);
transition:border-color .18s var(--ease),color .18s var(--ease)}
/* the whole card is the target; the title stays the visible affordance */
.strip h3 a::after{content:"";position:absolute;inset:0}
.strip h3 a:hover,.strip h3 a:focus-visible{color:var(--trace);border-bottom-color:var(--trace)}
.strip p{font-size:.95rem;color:var(--ink-2)}
.strip .why{margin-top:auto;font-family:var(--data);font-size:var(--t-micro);color:var(--mute)}
/* ---------- 06 about: colophon ------------------------------------------ */
.colophon{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,270px),1fr));
gap:2rem clamp(1.5rem,4vw,4rem);border-top:1px solid var(--rule);padding-top:1.75rem}
.colophon h3{font-family:var(--data);font-size:var(--t-micro);font-weight:400;color:var(--mute);
letter-spacing:.06em;margin-bottom:.75rem}
.colophon p{color:var(--ink-2)}
.colophon p + p{margin-top:.7rem}
.colophon a{color:var(--ink);text-decoration:none;border-bottom:1px solid var(--rule);
transition:border-color .26s var(--ease),color .26s var(--ease);
display:inline-block;padding-block:.35rem .1rem}
.colophon a:hover,.colophon a:focus-visible{color:var(--trace);border-bottom-color:var(--trace)}
.colophon .big{font-family:var(--display);font-size:1.25rem;font-weight:600;letter-spacing:-.02em}
footer{padding-block:2rem 3rem;font-family:var(--data);font-size:var(--t-micro);color:var(--mute)}
footer .shell{display:flex;flex-wrap:wrap;gap:.5rem 2rem;justify-content:space-between}
/* ============================ motion ===================================== */
/* Scroll reveal is IntersectionObserver only: it adds a class once and cannot
strand content mid-animation, which a scroll-driven timeline did on tall
viewports. Nothing is hidden without JS: .rv only hides once <html> has .js. */
.js .rv{opacity:0;transform:translateY(14px)}
.js .rv.on{opacity:1;transform:none;transition:opacity .55s var(--ease),transform .55s var(--ease)}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;
transition-duration:.001ms!important;scroll-behavior:auto!important}
.js .rv{opacity:1!important;transform:none!important;animation:none!important}
}
:target,.shelf{scroll-margin-top:calc(var(--head) + 14px)}
/* ============================ responsive ================================= */
@media (max-width:980px){
.case{grid-template-columns:minmax(0,1fr);gap:1.75rem}
.plot{position:static;grid-column:1;grid-row:2;width:100%}
.plotlabel{font-size:9.5px}
.plotbar-label{font-size:10px}
}
@media (max-width:640px){
:root{--t-body:1rem}
.masthead .shell{height:auto;padding-block:.5rem;flex-wrap:wrap;gap:.5rem}
.masthead nav{order:3;width:100%}
.masthead nav ul{gap:1.3rem .4rem;font-size:.68rem;justify-content:space-between;flex-wrap:nowrap}
@media (max-width:360px){.masthead nav ul{flex-wrap:wrap;justify-content:flex-start;gap:0 .9rem}}
.masthead nav a{padding:0 .28rem}
.themer{padding:.62rem .8rem}
.mark{padding-inline:.3rem .8rem}
.row{grid-template-columns:1fr;gap:.15rem}
.row > .when{padding-top:0}
.hero-strip{gap:.35rem 1.5rem}
.readout dd{font-size:1.2rem}
}
@media print{
/* override the tokens, not body: descendants read tokens, so a dark-theme
print was emitting dark panels and grey text onto white paper */
/* must match the specificity of :root:not([data-theme="bench"]) in the
prefers-color-scheme block, or a dark-theme print keeps the dark tokens */
:root,:root:not([data-theme="bench"]),[data-theme="scope"]{
--paper:#fff;--paper-lift:#fff;--ink:#000;--ink-2:#222;--mute:#444;
--rule:#999;--rule-soft:#ccc;--trace:#000;--flag:#000;
--trace-wash:transparent;--flag-wash:transparent;
}
.masthead{display:none}
.row,.case,.strip li,.dir li{break-inside:avoid}
.js .rv{opacity:1!important;transform:none!important;animation:none!important}
}
</style>
</head>
<body>
<a class="skip" href="#top">Skip to content</a>
<header class="masthead" aria-label="Site header">
<div class="shell">
<a class="mark" href="#top">MC</a>
<nav aria-label="Sections">
<ul role="list">
<li><a href="#measured">measured</a></li>
<li><a href="#shipped">shipped</a></li>
<li><a href="#books">books</a></li>
<li><a href="#upstream">upstream</a></li>
<li><a href="#friends">friends</a></li>
<li><a href="#tools">tools</a></li>
<li><a href="#about">about</a></li>
</ul>
</nav>
<button class="themer" id="themer" type="button" aria-pressed="false">
<span id="themer-label">theme</span>
</button>
</div>
</header>
<main id="top" tabindex="-1">
<!-- ========================= 00 hero ================================= -->
<section class="hero" aria-labelledby="h-hero">
<div class="shell">
<p class="tag"><b>Michael Chen</b> · AI software engineer</p>
<div class="hero-body">
<div>
<h1 id="h-hero">I build software for people I know, including me.</h1>
<p class="hero-role">Agent runtimes, tool dispatch and evaluation harnesses at
<b>Epoch AI / Zerg AI</b>.</p>
<p class="hero-lead">The rest of it started because a friend wanted something that
didn't exist yet. A few I measured properly afterwards. That's how I found out where
they fall short.</p>
</div>
<section class="readings" aria-labelledby="h-readings">
<h2 class="rhead" id="h-readings">latest readings</h2>
<dl>
<div><dt>kestrel real-time factor</dt><dd>×622</dd></div>
<div class="off"><dt>kestrel MCD</dt><dd>13.89 / 3.98</dd></div>
<div><dt>PHiLIP, contest entrants</dt><dd>8,231</dd></div>
</dl>
</section>
</div>
<div class="hero-strip">
<span>now <b>AI software engineer, full-time</b></span>
<span>school <b>CS, UC Riverside, 2026</b></span>
<span>here <b>Bay Area</b></span>
</div>
<p class="hero-links">
<a href="https://github.com/mchen04" rel="me noopener">github.com/mchen04</a>
<a href="https://www.linkedin.com/in/michael-luo-chen/" rel="me noopener">linkedin</a>
<a href="mailto:michaelluochen1@gmail.com">michaelluochen1@gmail.com</a>
</p>
</div>
</section>
<!-- ========================= 01 measured ============================= -->
<section class="shelf" id="measured" aria-labelledby="h-measured">
<div class="shell">
<div class="shelf-head wide">
<h2 id="h-measured">Measured</h2>
<p class="said">Two projects on a bench, with the readings. One of them missed its own
bar, and those numbers are here too.</p>
</div>
<ul role="list" class="cases">
<!-- ---- kestrel ---- -->
<li><article class="case rv">
<div>
<div class="case-title">
<h3><a href="https://github.com/mchen04/kestrel-tts" rel="noopener">Kestrel</a></h3>
<span class="tag">distilled TTS for Apple Silicon · <b>open weights</b></span>
</div>
<p class="case-what">I took Kokoro-82M apart and rebuilt it for MLX as a single-voice
audiobook engine, down to about <strong>10M active parameters</strong> from 82M. The
vocoder is now a source-filter head over an exact-phase harmonic template, swapped in on
cycle 107 for the per-bin complex mask it started as. It never leaves frame rate. On an
M2 MacBook with 16GB, a 163-second chapter renders in <strong>0.271s</strong> on the
<span class="num">student-fast</span> preset: a real-time factor of 622, and 53x faster
than stock Kokoro. That factor is measured against the roughly 168 seconds of audio the
preset actually emits, because its durations drift.</p>
<p class="case-what">Content survived the compression. ASR intelligibility sits at parity
with the teacher, <strong>5.46% WER against 5.65%</strong>, speaker cosine similarity
reads 0.98, and the new vocoder head pulled naturalness up to <strong>4.180 UTMOS
against the teacher's 4.477</strong>. The <span class="num">student</span> preset is
slower at 1.117s but keeps durations bit-exact against the teacher. Weights are about
51MB, Apache-2.0, in the repo and mirrored on
<a href="https://huggingface.co/mchen04/kestrel-tts" rel="noopener">Hugging Face <span class="go" aria-hidden="true">↗</span></a>.
The research loop closed on 4 August 2026 after 113 cycles.</p>
<dl class="readout">
<div><dt>chapter render</dt><dd>0.271<span class="u">s</span></dd></div>
<div><dt>real-time factor</dt><dd>×622</dd></div>
<div><dt>WER, teacher 5.65%</dt><dd>5.46<span class="u">%</span></dd></div>
<div><dt>active params</dt><dd>~10<span class="u">M</span></dd></div>
</dl>
<p class="case-what">Instrumenting my stack against the PyTorch reference turned up
five places where the MLX port of Kokoro diverges from it, so I wrote them up and
<a href="https://github.com/Blaizzy/mlx-audio/pull/859" rel="noopener">sent a patch
upstream to mlx-audio <span class="go" aria-hidden="true">↗</span></a>. The loud one is a constant
<strong>2.5 dB attenuation</strong>: the inverse STFT divided by the sum of the window
instead of its square, which for this configuration is exactly 0.75x. The subtle one
is a one-frame misalignment in the upsample path that sits directly in the pitch
contour, where predicted F0 went from <strong>0.134 relative RMSE against the
reference to 0.0000</strong>. Across a 55-utterance battery, MCD 7.29 to 4.09 dB and
F0 error 11.5 to 5.1 Hz. It merged on 5 August 2026.</p>
<div class="miss">
<p class="tag"><b>did not clear</b> <span>the reason this is the lead project</span></p>
<p>Texture is audibly hazier than Kokoro under direct A/B. Mel-cepstral distortion
comes in at <strong>13.89 against a 3.98 pass bar</strong>, and speaker similarity
at <strong>0.981 mean, 0.874 worst, against a 0.998 gate</strong>. Both are failing gates. The frozen zero-loss
battery isn't passed, and the README says so on its first screen.</p>
<p>The figures above are the repo's. The
<a href="https://github.com/mchen04/kestrel-tts/blob/main/experiments/23-final/AUDIT.md" rel="noopener">independent no-cheating audit <span class="go" aria-hidden="true">↗</span></a>
re-timed the same preset at <strong>0.340s</strong> and reproduced the speedup as
<strong>40x</strong>. It ran on 29 July 2026 against the build before the compile
round, and docs/history/REPORT.md logs what closed the gap after it: mx.compile with
fixed-shape padding across the student stages, 0.333s down to 0.239s. The
source-filter head swapped in after that costs 8% chapter wall, which is the 0.271s
it holds now. Same engine
either side of one optimization, not a dispute. What the audit says about the goal
does stand, and it's the line worth quoting: 1000x wasn't reached, and the
configuration that does pass the gates, <span class="num">ship-q8</span>, is roughly
1x. That preset is still the default provider, and Kestrel stays opt-in until the
gap closes.</p>
</div>
</div>
<figure class="plot">
<figcaption><b>fig. 1</b> · mel-cepstral distortion against the frozen pass bar. The bar is
3.98. Kestrel reads 13.89. Lower is better, and this is the one that did not clear.</figcaption>
<svg viewBox="0 0 220 96" role="img" aria-label="Bar chart. The pass bar for mel-cepstral distortion is 3.98. Kestrel measures 13.89, about three and a half times over the bar.">
<line class="gridline" x1="54" y1="8" x2="54" y2="80"></line>
<line class="gridline" x1="54" y1="80" x2="190" y2="80"></line>
<text class="plotlabel" x="0" y="31">pass bar</text>
<rect class="barok" x="54" y="20" width="39" height="14"></rect>
<text class="plotbar-label" x="97" y="31">3.98</text>
<text class="plotlabel" x="0" y="59">kestrel</text>
<rect class="barbad" x="54" y="48" width="135" height="14"></rect>
<text class="plotbar-label" x="193" y="59">13.89</text>
<line class="gridline" x1="93" y1="8" x2="93" y2="80" stroke-dasharray="2 3"></line>
<text class="plotlabel" x="54" y="92">0</text>
<text class="plotlabel" x="190" y="92" text-anchor="end">14 MCD</text>
</svg>
<p class="src"><a href="https://huggingface.co/mchen04/kestrel-tts" rel="noopener">weights on
Hugging Face <span class="go" aria-hidden="true">↗</span></a> · every figure above is in
<a href="https://github.com/mchen04/kestrel-tts/blob/main/docs/MODEL_CARD.md" rel="noopener">docs/MODEL_CARD.md <span class="go" aria-hidden="true">↗</span></a></p>
</figure>
</article></li>
<!-- ---- philip ---- -->
<li><article class="case solo rv">
<div>
<div class="case-title">
<h3><a href="https://www.hackster.io/engineers-ucr/philip-personalized-human-in-loop-image-production-b90133" rel="noopener">PHiLIP</a></h3>
<span class="tag">human-in-the-loop image generation on AMD ROCm</span>
</div>
<p class="case-what">Generate, look, adjust, generate again. The point was making that
loop short enough that a person stays inside it, which meant getting inference to about
<strong>two seconds</strong> on ROCm with a fine-tuned PixArt-alpha. It also ships
<strong>71 style presets</strong>, because exploring shouldn't mean writing prompts all
day.</p>
<p class="case-what">It won the University category of the AMD Pervasive AI Developer
Contest, out of <strong>8,231 entrants</strong>, and we demoed it live at AMD Advancing
AI 2024 at Moscone Center.</p>
</div>
<div class="aside">
<dl class="readout">
<div><dt>inference</dt><dd>~2<span class="u">s</span></dd></div>
<div><dt>style presets</dt><dd>71</dd></div>
<div><dt>contest field</dt><dd>8,231</dd></div>
</dl>
<p class="src">the timing, the award and the demo are documented in the
<a href="https://www.hackster.io/engineers-ucr/philip-personalized-human-in-loop-image-production-b90133" rel="noopener">contest writeup <span class="go" aria-hidden="true">↗</span></a>.
The <a href="https://github.com/mchen04/PHiLIP" rel="noopener">repo <span class="go" aria-hidden="true">↗</span></a> is the 2024
source and carries no README. The presets are the only claim it backs on its own.</p>
</div>
</article></li>
</ul>
</div>
</section>
<!-- ========================= 02 shipped ============================== -->
<section class="shelf" id="shipped" aria-labelledby="h-shipped">
<div class="shell">
<div class="shelf-head wide">
<h2 id="h-shipped">Shipped</h2>
<p class="said">Work people paid for. Some of it I can only describe in outline.</p>
</div>
<ul class="ledger" role="list">
<li><article class="row rv">
<p class="when">2025 to now</p>
<div>
<h3><a href="https://zergai.com" rel="noopener">Zerg and ZTC</a></h3>
<p>An autonomous coding-agent platform, and the agentic terminal that drives it. I worked
on parallel tool dispatch so agents stop queueing behind each other, background
sub-agents for work that outlives a single turn, worktree-isolated batch execution so
concurrent writers do not collide, a sandboxed multi-agent workflow runtime, MCP and
provider routing, and the live web mirror. A human can watch a run while it happens.</p>
</div>
<p class="built">Epoch AI / Zerg AI · TypeScript, Python</p>
</article></li>
<li><article class="row rv">
<p class="when">2025 to now</p>
<div>
<h3><a href="https://zergchat.com" rel="noopener">ZergChat</a></h3>
<p>An agent-native chat product built on the platform, where an agent sits in the channel and answers
like anyone else in it.</p>
</div>
<p class="built">Epoch AI / Zerg AI</p>
</article></li>
<li><article class="row rv">
<p class="when">2025 to now</p>
<div>
<h3><a href="https://zergboard.com" rel="noopener">Zergboard</a></h3>
<p>The board-shaped product on the same platform. Same runtime underneath, a different
surface for work that lives in cards.</p>
</div>
<p class="built">Epoch AI / Zerg AI</p>
</article></li>
<li><article class="row rv">
<p class="when">2025 to now</p>
<div>
<h3>Forward-deployed engineering</h3>
<p>Two undisclosed customers. One is an org-management and resource-planning platform,
where I built continual-validation harnesses that score agent output against
deterministic state, tool, artifact, browser, and visual oracles. A regression shows up as a failing check on the
way in. The other is an AI procurement intake
and approval portal with grounded vendor research, approvals that stay owned by a
human, and RBAC with separation-of-duties controls.</p>
</div>
<p class="built">Epoch AI / Zerg AI · customer names withheld, no public artifact</p>
</article></li>
<li><article class="row rv">
<p class="when">2023 to 2025</p>
<div>
<h3>MOBIVOLT</h3>
<p>A $5M+ DOD-funded fuel-cell program. I wrote a Python and PyQt hardware-in-the-loop
simulator with a serial firmware bridge, injecting sensor values every 100ms and
polling at 200ms against plant models. Firmware could then be
exercised without a rig. I ported PIC32MZ firmware onto Harmony v3 across 50+ application and
driver modules with 2,500+ lines of C tests, and kept 5 automated test stands
processing 100+ concurrent data streams with zero downtime across 18 months.</p>
</div>
<p class="built">C, C#, Python, PyQt, PIC32MZ, Harmony v3 · defense program, no public artifact</p>
</article></li>
<li><article class="row rv">
<p class="when">2023 to 2025</p>
<div>
<h3>AI at UCR</h3>
<p>I started UC Riverside's first official AI organization and ran it to 50+ members,
10+ technical workshops, and 45% growth in attendance. Most of the work was booking rooms and writing curriculum.</p>
</div>
<p class="built">founder and president</p>
</article></li>
</ul>
</div>
</section>
<!-- ========================= 03 books ================================ -->
<section class="shelf" id="books" aria-labelledby="h-books">
<div class="shell">
<div class="shelf-head wide">
<h2 id="h-books">Books, and the audio stack they caused</h2>
<p class="said">Where Kestrel, Epub Listener and Hark came from. It starts with wanting to
listen to a web novel on a walk and ends with me distilling a vocoder, which is a
ridiculous way to solve that problem.</p>
</div>
<div class="essay-wrap">
<div class="essay">
<p>I read a lot of web novels and light novels. The good ones run to hundreds of chapters and
the translation pace means you are always waiting, so at some point I wanted to listen to
them instead of reading them, mostly so I could do it while walking somewhere.</p>
<p>Nothing would do it. So I wrote a CLI that takes an EPUB and gives back a narrated MP3
with real chapter markers, which is <a href="https://github.com/mchen04/epub-listener" rel="noopener">Epub Listener</a>.
The files turned out too big and too awkward to move around, so I built
<a href="https://github.com/mchen04/hark-audiobook" rel="noopener">Hark</a> to hold them,
which parses the MP3 in the browser and keeps the audio on the device. The server only ever
sees titles, chapters, my position and my playback history.</p>
<p>A 600-chapter book was still hours of waiting. That is when I went and distilled
the vocoder. That is <a href="https://github.com/mchen04/kestrel-tts" rel="noopener">Kestrel</a>,
the same project as the benchmark table further up. It's fast enough now that the pipeline stopped
being the thing I waited on. It also still sounds slightly worse than the model it came from, which is the
part I did not manage to fix.</p>
<p>Hark got a read-along mode out of it, because Epub Listener already knew the word timings
the TTS engine produced and it seemed wasteful to throw them away. The player
highlights the narrated sentence, marks the word being spoken, and jumps if you tap
somewhere else. It works on a plane, in airplane mode.</p>
<p>In August the loop closed on itself. Kestrel now runs inside Hark, so the browser takes a
PDF or an EPUB and narrates it on the device. The CLI is no longer in the path, and
neither is my laptop.</p>
</div>
<figure class="shot">
<figcaption><b>fig. 2</b> · Hark installed to the iPhone Home Screen, playing a test
import. The MP3 was parsed in the browser and stored on the device; nothing on this
screen came off a server.</figcaption>
<img src="images/hark-player.png" width="477" height="800" loading="lazy" decoding="async"
alt="Hark's player running as an installed iPhone app: cover art, the book title, a
position slider at four seconds, a large pause button, and controls for speed,
sleep timer, chapters and history.">
<p class="src"><a href="https://audiobook-pwa-plum.vercel.app" rel="noopener">Hark
live <span class="go" aria-hidden="true">↗</span></a> · captured from the iPhone e2e
suite in <a href="https://github.com/mchen04/hark-audiobook" rel="noopener">the repo <span class="go" aria-hidden="true">↗</span></a></p>
</figure>
</div>
<ul class="three" role="list">
<li>
<h3 class="n"><a href="https://github.com/mchen04/epub-listener" rel="noopener">Epub Listener<span class="go" aria-hidden="true"> github ↗</span></a></h3>
<span class="d">Python CLI. EPUB in, chaptered MP3 out, via Edge-TTS, local Kokoro,
Kestrel on Apple Silicon, any Hugging Face text-to-speech pipeline model, or any
local command that writes audio.
Checksum-based resume so a failed run does not start over. Embeds a timestamped
read-along transcript, median word-onset error under 30ms.</span>
</li>
<li>
<h3 class="n"><a href="https://github.com/mchen04/kestrel-tts" rel="noopener">Kestrel<span class="go" aria-hidden="true"> github ↗</span></a></h3>
<span class="d">The reason the pipeline is fast enough to use, and the one that fails
its own quality battery. The numbers and the audit are up under Measured.</span>
</li>
<li>
<h3 class="n"><a href="https://audiobook-pwa-plum.vercel.app" rel="noopener">Hark<span class="go" aria-hidden="true"> live ↗</span><span class="go"> (sign-in)</span></a></h3>
<span class="d">Installable offline-first audiobook PWA. Import a chaptered MP3, or a
PDF, EPUB, DOCX, text, Markdown or HTML file it narrates on the device with Kestrel
through WebGPU, WASM where there is none. Everything is parsed and stored on-device,
server sees metadata only. Karaoke-style read-along with sentence highlight and
word-level marking. Playback and read-along work offline; only the first narration
downloads the model, about 39MB of weights and a 24MB runtime.</span>
</li>
</ul>
</div>
</section>
<!-- ========================= 04 upstream ============================= -->
<section class="shelf" id="upstream" aria-labelledby="h-upstream">
<div class="shell">
<div class="shelf-head">
<h2 id="h-upstream">Other people's repositories</h2>
<p class="said">Code I wrote somewhere I do not own. The first one started as me using the
thing and finding it wrong. The rest are friends' projects I kept showing up to.</p>
</div>
<ul class="strip" role="list">
<li><article class="rv">
<h3><a href="https://github.com/Blaizzy/mlx-audio/pull/859" rel="noopener">mlx-audio <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Five places where the MLX iSTFTNet decoders (Kokoro, KittenTTS) diverged from the
PyTorch reference they were ported from. Found by instrumenting both stacks
layer by layer on identical phoneme and style inputs and diffing every intermediate
tensor. Merged upstream.</p>
<p>Two were audible. Every Kokoro render came out
<span class="num">2.5 dB</span> quiet, because MLX's <span class="num">istft</span>
divides the overlap-add by <span class="num">Σw</span> and
<span class="num">torch.istft</span> divides by <span class="num">Σw²</span>. And the
upsample path in the pitch and energy predictor was misaligned by one frame against its
own shortcut: F0 relative RMSE <span class="num">0.134 → 0.0000</span>.</p>
<p class="why">Checked against frozen fp32 reference audio over 55 utterances, with the
thresholds calibrated to the reference model's own render-to-render noise floor, since
the decoder is stochastic. MCD <span class="num">7.29 → 4.09 dB</span>, F0 RMSE
<span class="num">11.5 → 5.1 Hz</span>. I hit this while building Kestrel.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://github.com/jormyy/valence" rel="noopener">Valence <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Jeremy's sports stream aggregator. Three merged pull requests, and 54 of the commits
on main against his 13.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://github.com/jormyy/pancake" rel="noopener">Pancake <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Jeremy's dynasty fantasy basketball app, still being built. Thirty-eight merged pull
requests are mine, and 185 of the commits on main against his 175.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://github.com/jormyy/ding" rel="noopener">Ding <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Jeremy's collaborative poker-ranking game. 96 of its 140 commits are mine, committed
straight to main rather than sent as patches.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://github.com/matthewh8/25-words-or-less" rel="noopener">25 Words or Less <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Matthew's same-screen party word game. Bid low, then hope your partner is on your
wavelength. 34 of its 39 commits are mine.</p>
</article></li>
</ul>
</div>
</section>
<!-- ========================= 05 friends ============================== -->
<section class="shelf" id="friends" aria-labelledby="h-friends">
<div class="shell">
<div class="shelf-head wide">
<h2 id="h-friends">For my friends</h2>
<p class="said">Built because someone asked, usually the night before we needed it.</p>
</div>
<ul class="dir" role="list">
<li><a href="https://pokernow.vercel.app" rel="noopener"><span class="i num">01</span><h3 class="n">Felt</h3><span class="d">private play-money hold'em rooms, no signup, every table feature free</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://github.com/mchen04/codenames" rel="noopener"><span class="i num">02</span><h3 class="n">Codenames</h3><span class="d">realtime rooms, curated word packs, chat so people can argue about the clue</span><span class="h" aria-hidden="true">github ↗</span></a></li>
<li><a href="https://loldle-app.vercel.app" rel="noopener"><span class="i num">03</span><h3 class="n">LoLdle</h3><span class="d">19 modes. The stats persist, which was the actual request.</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://drinking-games-eosin.vercel.app" rel="noopener"><span class="i num">04</span><h3 class="n">Pour Decisions</h3><span class="d">36 games and one shared player roster.</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://kings-cup-liard.vercel.app" rel="noopener"><span class="i num">05</span><h3 class="n">Kings Cup</h3><span class="d">The cup meter surviving a refresh took longer than the rest of it.</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://82-0-orpin.vercel.app" rel="noopener"><span class="i num">06</span><h3 class="n">Better 82-0</h3><span class="d">multiplayer NBA cap draft with server-authoritative roster legality</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://splitwisest-kappa.vercel.app" rel="noopener"><span class="i num">07</span><h3 class="n">SplitWisest</h3><span class="d">shared expenses, simplified debts, settle offline</span><span class="h"><span aria-hidden="true">live ↗</span> (sign-in)</span></a></li>
<li><a href="https://github.com/mchen04/Sayable" rel="noopener"><span class="i num">08</span><h3 class="n">Sayable</h3><span class="d">no login. guests answer from a link and the host gets an aggregate back</span><span class="h" aria-hidden="true">github ↗</span></a></li>
<li><a href="https://first-or-eighth.vercel.app" rel="noopener"><span class="i num">09</span><h3 class="n">First-Or-Eighth</h3><span class="d">the hub for the rest of these</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://casino-topaz-gamma.vercel.app" rel="noopener"><span class="i num">10</span><h3 class="n">Neon Royale</h3><span class="d">34 play-money casino games and a localStorage chip wallet</span><span class="h" aria-hidden="true">live ↗</span></a></li>
<li><a href="https://selfie-a-day.vercel.app" rel="noopener"><span class="i num">11</span><h3 class="n">Selfie a Day</h3><span class="d">one photo a day, lined up against the ghost of yesterday's so the face stops jumping. Never leaves the device.</span><span class="h" aria-hidden="true">live ↗</span></a></li>
</ul>
</div>
</section>
<!-- ========================= 06 tools ================================ -->
<section class="shelf" id="tools" aria-labelledby="h-tools">
<div class="shell">
<div class="shelf-head">
<h2 id="h-tools">Tools I made for myself</h2>
<p class="said">Four things I use most days. I would rebuild all four.</p>
</div>
<ul class="strip" role="list">
<li><article class="rv">
<h3><a href="https://github.com/mchen04/tcd" rel="noopener">tcd <span class="go" aria-hidden="true">↗</span></a></h3>
<p>zsh and tmux helper that launches, finds, and reattaches long-running AI CLI sessions by
partial name. <span class="num">tcd zer</span> gets me back into the
session I left open on that repo, whatever the generated name was.</p>
<p class="why">Sessions survive reboots and phone reconnects. I use it from my phone on the train. The partial-name matching exists because of that.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://github.com/mchen04/claude-switch" rel="noopener">cs <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Rust CLI for macOS that swaps Claude Code accounts by replacing the Keychain entry in
place, plus a per-profile usage dashboard.</p>
<p class="why">The built-in flow is logout, login, wait. This is under a second. Each
account keeps its own settings, and one master profile's skills, commands and agents
follow me into all of them.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://github.com/mchen04/espn-fantasy-lineup-bot" rel="noopener">ESPN lineup bot <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Chrome extension plus a Node server that wakes five minutes before the first game each
day and sets my fantasy basketball lineup off live injury reports. It reads the injury wire
rather than a projection, because projections lag the news by hours and the waiver order
does not wait.</p>
</article></li>
<li><article class="rv">
<h3><a href="https://kyle-financial.vercel.app" rel="noopener">House by 30 <span class="go" aria-hidden="true">↗</span></a></h3>
<p>Offline-capable budgeting PWA that turns annual income, estimated taxes, payroll
benefits, and planned expenses into an honest monthly remainder.</p>
<p class="why">Named after the goal. Built it for a friend, then kept using it myself. Opens on a sign-in.</p>
</article></li>
</ul>
</div>
</section>
<!-- ========================= 07 about ================================ -->
<section class="shelf" id="about" aria-labelledby="h-about">
<div class="shell">
<div class="shelf-head">
<h2 id="h-about">About</h2>
</div>
<div class="colophon">
<div>
<h3>school</h3>
<p class="big">UC Riverside</p>
<p>B.S. Computer Science, June 2026. GPA 3.66, with a minor in Strategy &
Entrepreneurship.</p>
<p>Founder and president of AI at UCR, the university's first official AI organization.</p>
</div>
<div>
<h3>awards</h3>
<p class="big">AMD Pervasive AI, University category</p>
<p>For PHiLIP, an award-winning human-in-the-loop image-generation platform on AMD ROCm
with two-second inference and PixArt-alpha fine-tuning.
<a href="https://www.hackster.io/contests/amd2023#category-1092" rel="noopener">Official results <span class="go" aria-hidden="true">↗</span></a></p>
<p>Cutie Hack 2022, first place in the sustainability track.</p>
</div>
<div>
<h3>reaching me</h3>
<p class="big"><a href="mailto:michaelluochen1@gmail.com">michaelluochen1@gmail.com</a></p>
<p><a href="https://github.com/mchen04" rel="me noopener">github.com/mchen04</a></p>
<p><a href="https://www.linkedin.com/in/michael-luo-chen/" rel="me noopener">linkedin <span class="go" aria-hidden="true">↗</span></a></p>
</div>
</div>
</div>
</section>
</main>
<footer aria-label="Colophon">
<div class="shell">
<span>© 2026 Michael Chen</span>
<span>Bricolage Grotesque, Newsreader, IBM Plex Mono. Every number under Measured comes from a repo you can open.</span>
</div>
</footer>
<script>
document.documentElement.classList.add('js');
/* The sticky masthead changes height with viewport and font load. Anchor
offsets are derived from its measured height so a nav tap can never park a
heading underneath it. */
(function(){
var head=document.querySelector('.masthead');
function sync(){ document.documentElement.style.setProperty('--head', head.offsetHeight+'px'); }