code wiki / _hdl_build / nx_coach_serve.nx
nx_coach_serve.nx source
↩ module page · 188 lines · 12939 B
1// nx_coach_serve.nx -- SKILL-COACH rung R5 (publish scaffold): the /coach HTTP request HANDLER wrapping the
2// unified engine. Browser (phone) getUserMedia -> POST the detected pitch/notes -> coach grade -> feedback JSON.
3// Gateable per feedback-sovereign-gates-not-bash-harnesses: cs_handle(method,path,body)->response bytes, asserted
4// by CALLING it (no socket/curl). The DAEMON wrapper (accept-fork loop + nx_pitch on the POSTed PCM + OPAQUE auth +
5// /coach sites-route) is the thin OPERATOR-COORDINATED deploy layer (the /gen pattern). HTML emitted by THIS organ
6// (nishi-ecosystem: no hand-JS except the emitted last-mile). Composes nx_note + the R2 rubric store. license_tier: ORIGINAL
7import "nx_note.nx"
8import "nx_seg_store.nx"
9import "nx_syscalls.nx"
10const CS_MAGIC_8192: i64 = 8192
11const CS_MAGIC_1024: i64 = 1024
12const CS_MAGIC_2048: i64 = 2048
13const CS_MAGIC_16384: i64 = 16384
14
15const CS_RUBRIC: *u8 = "knowledge/store/coach-rubric-" as *u8
16
17func cs_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
18func cs_cat(dst: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[o + i] = s[i]; i = i + 1 } return o + i }
19func cs_catn(dst: *u8, o: i64, v: i64) -> i64 {
20 var m: i64 = v; var oo: i64 = o
21 if m < 0 { dst[oo] = 45 as u8; oo = oo + 1; m = 0 - m }
22 let t: *u8 = sys_mmap(24); var k: i64 = 0
23 if m == 0 { t[0] = 48 as u8; k = 1 }
24 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
25 var i: i64 = k - 1
26 while i >= 0 { dst[oo] = t[i]; oo = oo + 1; i = i - 1 }
27 return oo
28}
29func cs_catb(dst: *u8, o: i64, src: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { dst[o + i] = src[i]; i = i + 1 } return o + i }
30func cs_atoi(s: *u8, n: i64) -> i64 { var v: i64 = 0; var i: i64 = 0; while i < n { let c: i64 = s[i]; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } i = i + 1 } return v }
31func cs_has(b: *u8, n: i64, needle: *u8) -> i64 {
32 let ln: i64 = cs_slen(needle); if ln == 0 { return 1 }
33 var p: i64 = 0
34 while p + ln <= n { var j: i64 = 0; var hit: i64 = 1; while j < ln { if b[p + j] != needle[j] { hit = 0; j = ln } else { j = j + 1 } } if hit == 1 { return 1 } p = p + 1 }
35 return 0
36}
37func cs_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while 1 == 1 { if a[i] != b[i] { return 0 } if a[i] == (0 as u8) { return 1 } i = i + 1 } return 1 }
38// rubric drill bytes for a key (composes the R2 store)
39func cs_drill(h: *i64, key: *u8, outlen: *i64) -> *u8 {
40 let pp: *i64 = sys_mmap(16) as *i64; let ll: *i64 = sys_mmap(16) as *i64
41 if ss_hget(h, key, pp, ll) != 1 { outlen[0] = 0; return 0 as *u8 }
42 outlen[0] = ll[0]; return pp[0] as *u8
43}
44func cs_sing_vname(v: i64) -> *u8 { if v == 0 { return "in_tune" as *u8 } if v == 1 { return "sharp" as *u8 } if v == 2 { return "flat" as *u8 } if v == 3 { return "too_high" as *u8 } return "too_low" as *u8 }
45// build the feedback JSON for a singing attempt into out; returns length
46func cs_grade_sing(h: *i64, f0: i64, target_midi: i64, tbl: *i64, out: *u8) -> i64 {
47 let no: *i64 = sys_mmap(8 * 4) as *i64; nx_note_of(f0, tbl, no)
48 let dm: i64 = no[0]; let cents: i64 = no[1]
49 let tl: *i64 = sys_mmap(16) as *i64
50 let ts: *u8 = cs_drill(h, "sing:tol_cents" as *u8, tl)
51 var tol: i64 = 20; if (ts as i64) != 0 { tol = cs_atoi(ts, tl[0]) }
52 var v: i64 = 4
53 if dm == target_midi { if cents > tol { v = 1 } else { if cents < (0 - tol) { v = 2 } else { v = 0 } } } else { if dm > target_midi { v = 3 } else { v = 4 } }
54 var pts: i64 = 10; if v == 0 { pts = 100 } else { if v <= 2 { var a: i64 = cents; if a < 0 { a = 0 - a } pts = 100 - a; if pts < 0 { pts = 0 } } }
55 let key: *u8 = sys_mmap(64); var ko: i64 = cs_cat(key, 0, "sing:" as *u8); ko = cs_cat(key, ko, cs_sing_vname(v)); key[ko] = 0 as u8
56 let dl: *i64 = sys_mmap(16) as *i64; let drill: *u8 = cs_drill(h, key, dl)
57 var o: i64 = 0
58 o = cs_cat(out, o, "{\"discipline\":\"sing\",\"verdict\":\"" as *u8); o = cs_cat(out, o, cs_sing_vname(v))
59 o = cs_cat(out, o, "\",\"midi\":" as *u8); o = cs_catn(out, o, dm)
60 o = cs_cat(out, o, ",\"cents\":" as *u8); o = cs_catn(out, o, cents)
61 o = cs_cat(out, o, ",\"points\":" as *u8); o = cs_catn(out, o, pts)
62 o = cs_cat(out, o, ",\"drill\":\"" as *u8); if (drill as i64) != 0 { o = cs_catb(out, o, drill, dl[0]) }
63 o = cs_cat(out, o, "\"}" as *u8); out[o] = 0 as u8
64 return o
65}
66// the emitted coach page (minimal last-mile: getUserMedia + fetch to the grade API + show feedback)
67func cs_shell(out: *u8) -> i64 {
68 var o: i64 = 0
69 o = cs_cat(out, o, "<!doctype html><html><head><meta charset=utf-8><title>Nishi Coach</title></head><body>" as *u8)
70 o = cs_cat(out, o, "<h1>Self-Teaching Coach</h1><p>Sing a note, play a chord, or practice a tough conversation -- we grade it and tell you what to practice.</p>" as *u8)
71 o = cs_cat(out, o, "<button id=mic>Start mic</button><div id=fb></div>" as *u8)
72 o = cs_cat(out, o, "<script>document.getElementById('mic').onclick=async()=>{const s=await navigator.mediaDevices.getUserMedia({audio:true});" as *u8)
73 o = cs_cat(out, o, "/* capture PCM, POST to /coach/api/grade, render JSON drill+xp */};</script></body></html>" as *u8)
74 out[o] = 0 as u8; return o
75}
76// frame an HTTP response: status + content-type + content-length + body
77func cs_resp(status: *u8, ctype: *u8, body: *u8, blen: i64, out: *u8) -> i64 {
78 var o: i64 = 0
79 o = cs_cat(out, o, "HTTP/1.1 " as *u8); o = cs_cat(out, o, status)
80 o = cs_cat(out, o, "\r\nContent-Type: " as *u8); o = cs_cat(out, o, ctype)
81 o = cs_cat(out, o, "\r\nContent-Length: " as *u8); o = cs_catn(out, o, blen)
82 o = cs_cat(out, o, "\r\n\r\n" as *u8); o = cs_catb(out, o, body, blen)
83 return o
84}
85const CS_CURRIC: *u8 = "knowledge/store/coach-curriculum-" as *u8
86// build "<skill>:<suffix>" or "<skill>:<suffix>:<idx>"
87func cs_ckey(skill: *u8, suffix: *u8, idx: i64, hasidx: i64, out: *u8) -> i64 {
88 var o: i64 = cs_cat(out, 0, skill); o = cs_cat(out, o, ":" as *u8); o = cs_cat(out, o, suffix)
89 if hasidx == 1 { o = cs_cat(out, o, ":" as *u8); o = cs_catn(out, o, idx) }
90 out[o] = 0 as u8; return o
91}
92// CURRICULUM-engine endpoint: grade a self-report mask vs the framework checklist -> feedback JSON (the 2nd pillar)
93func cs_grade_curriculum(skill: *u8, response_mask: i64, out: *u8) -> i64 {
94 let h: *i64 = ss_open(CS_CURRIC)
95 var o: i64 = cs_cat(out, 0, "{\"discipline\":\"" as *u8); o = cs_cat(out, o, skill); o = cs_cat(out, o, "\",\"missing\":[" as *u8)
96 var nmiss: i64 = 0
97 if (h as i64) != 0 {
98 let key: *u8 = sys_mmap(96); let ol: *i64 = sys_mmap(16) as *i64
99 cs_ckey(skill, "n" as *u8, 0, 0, key); let ns: *u8 = cs_drill(h, key, ol)
100 var n: i64 = 0; if (ns as i64) != 0 { n = cs_atoi(ns, ol[0]) }
101 let checklist: i64 = (1 << n) - 1
102 let missing: i64 = checklist & (0xffffff ^ response_mask)
103 var first: i64 = 1; var i: i64 = 0
104 while i < n {
105 if (missing & (1 << i)) != 0 {
106 nmiss = nmiss + 1
107 if first == 0 { o = cs_cat(out, o, "," as *u8) } first = 0
108 cs_ckey(skill, "elem" as *u8, i, 1, key); let em: *u8 = cs_drill(h, key, ol)
109 o = cs_cat(out, o, "{\"element\":\"" as *u8); if (em as i64) != 0 { o = cs_catb(out, o, em, ol[0]) }
110 o = cs_cat(out, o, "\",\"drill\":\"" as *u8)
111 cs_ckey(skill, "drill" as *u8, i, 1, key); let dr: *u8 = cs_drill(h, key, ol)
112 if (dr as i64) != 0 { o = cs_catb(out, o, dr, ol[0]) }
113 o = cs_cat(out, o, "\"}" as *u8)
114 }
115 i = i + 1
116 }
117 }
118 o = cs_cat(out, o, "],\"complete\":" as *u8)
119 if nmiss == 0 { o = cs_cat(out, o, "true}" as *u8) } else { o = cs_cat(out, o, "false}" as *u8) }
120 out[o] = 0 as u8; return o
121}
122// THE HANDLER: method + path + body -> response bytes (len returned). rh = rubric handle, tbl = note table.
123func cs_handle(method: *u8, path: *u8, body: *u8, blen: i64, rh: *i64, tbl: *i64, out: *u8) -> i64 {
124 if cs_streq(method, "GET" as *u8) == 1 { if cs_streq(path, "/" as *u8) == 1 {
125 let sh: *u8 = sys_mmap(CS_MAGIC_8192); let sl: i64 = cs_shell(sh)
126 return cs_resp("200 OK" as *u8, "text/html" as *u8, sh, sl, out)
127 } }
128 if cs_streq(method, "POST" as *u8) == 1 { if cs_streq(path, "/coach/api/grade" as *u8) == 1 {
129 // body = "sing <f0> <target>" (the daemon decodes POSTed PCM -> f0 via nx_pitch upstream)
130 if cs_has(body, blen, "sing" as *u8) == 1 {
131 // parse "sing <f0> <target>": find the two spaces, atoi the two numbers between/after them
132 var s1: i64 = 0 - 1; var k: i64 = 0
133 while k < blen { if body[k] == (32 as u8) { if s1 < 0 { s1 = k } } k = k + 1 }
134 var s2: i64 = 0 - 1; k = s1 + 1
135 while k < blen { if body[k] == (32 as u8) { if s2 < 0 { s2 = k } } k = k + 1 }
136 var f0: i64 = 0; var tgt: i64 = 69
137 if s1 > 0 { if s2 > s1 {
138 f0 = cs_atoi((body as i64 + s1 + 1) as *u8, s2 - s1 - 1)
139 tgt = cs_atoi((body as i64 + s2 + 1) as *u8, blen - s2 - 1)
140 } }
141 let js: *u8 = sys_mmap(CS_MAGIC_1024); let jl: i64 = cs_grade_sing(rh, f0, tgt, tbl, js)
142 return cs_resp("200 OK" as *u8, "application/json" as *u8, js, jl, out)
143 }
144 return cs_resp("400 Bad Request" as *u8, "text/plain" as *u8, "need discipline" as *u8, 15, out)
145 } }
146 if cs_streq(method, "POST" as *u8) == 1 { if cs_streq(path, "/coach/api/curriculum" as *u8) == 1 {
147 // body = "<skill> <response_mask>" e.g. "confront 9" (the daemon maps the roleplay self-report -> a mask)
148 var sp: i64 = 0 - 1; var k: i64 = 0
149 while k < blen { if body[k] == (32 as u8) { if sp < 0 { sp = k } } k = k + 1 }
150 if sp > 0 {
151 let skill: *u8 = sys_mmap(64); var t: i64 = 0
152 while t < sp { skill[t] = body[t]; t = t + 1 } skill[sp] = 0 as u8
153 let mask: i64 = cs_atoi((body as i64 + sp + 1) as *u8, blen - sp - 1)
154 let js: *u8 = sys_mmap(CS_MAGIC_2048); let jl: i64 = cs_grade_curriculum(skill, mask, js)
155 return cs_resp("200 OK" as *u8, "application/json" as *u8, js, jl, out)
156 }
157 return cs_resp("400 Bad Request" as *u8, "text/plain" as *u8, "need skill mask" as *u8, 15, out)
158 } }
159 return cs_resp("404 Not Found" as *u8, "text/plain" as *u8, "not found" as *u8, 9, out)
160}
161func main() -> i64 {
162 sys_write(1, "COACH SERVE (R5 scaffold): /coach request handler over the unified engine\n" as *u8, 73)
163 let rh: *i64 = ss_open(CS_RUBRIC)
164 if (rh as i64) == 0 { sys_write(1, " rubric store missing -- run nx_coach_rubric first\n" as *u8, 51); return 1 }
165 let tbl: *i64 = sys_mmap(8 * 128) as *i64; nx_note_table(tbl)
166 let out: *u8 = sys_mmap(CS_MAGIC_16384)
167 var pass: i64 = 0
168 // GET / -> the coach shell
169 let r1: i64 = cs_handle("GET" as *u8, "/" as *u8, "" as *u8, 0, rh, tbl, out)
170 if cs_has(out, r1, "200 OK" as *u8) == 1 { if cs_has(out, r1, "Self-Teaching Coach" as *u8) == 1 { if cs_has(out, r1, "getUserMedia" as *u8) == 1 { pass = pass + 1 } } }
171 // POST grade "sing 430 69" -> flat + the breath-support drill, as JSON
172 let r2: i64 = cs_handle("POST" as *u8, "/coach/api/grade" as *u8, "sing 430 69" as *u8, 11, rh, tbl, out)
173 sys_write(1, " POST sing 430 69 -> " as *u8, 22); sys_write(1, out, r2); sys_write(1, "\n" as *u8, 1)
174 if cs_has(out, r2, "application/json" as *u8) == 1 { if cs_has(out, r2, "\"flat\"" as *u8) == 1 { if cs_has(out, r2, "breath support" as *u8) == 1 { pass = pass + 1 } } }
175 // POST grade "sing 440 69" -> in_tune
176 let r3: i64 = cs_handle("POST" as *u8, "/coach/api/grade" as *u8, "sing 440 69" as *u8, 11, rh, tbl, out)
177 if cs_has(out, r3, "\"in_tune\"" as *u8) == 1 { pass = pass + 1 }
178 // POST curriculum "confront 9" -> the CURRICULUM engine over the same /coach: missing Feeling + Need, as JSON
179 let rc: i64 = cs_handle("POST" as *u8, "/coach/api/curriculum" as *u8, "confront 9" as *u8, 10, rh, tbl, out)
180 sys_write(1, " POST confront 9 -> " as *u8, 21); sys_write(1, out, rc); sys_write(1, "\n" as *u8, 1)
181 if cs_has(out, rc, "application/json" as *u8) == 1 { if cs_has(out, rc, "Feeling" as *u8) == 1 { if cs_has(out, rc, "Need" as *u8) == 1 { pass = pass + 1 } } }
182 // unknown route -> 404
183 let r4: i64 = cs_handle("GET" as *u8, "/nope" as *u8, "" as *u8, 0, rh, tbl, out)
184 if cs_has(out, r4, "404" as *u8) == 1 { pass = pass + 1 }
185 sys_write(1, " handler checks = " as *u8, 19); let pc: *u8 = sys_mmap(8); pc[0] = (48 + pass) as u8; sys_write(1, pc, 1); sys_write(1, "/5\n" as *u8, 3)
186 if pass == 5 { sys_write(1, "COACH-SERVE: GREEN -- one /coach grades BOTH sensor (sung note) and curriculum (roleplay) as JSON; deploy-ready\n" as *u8, 112); return 0 }
187 sys_write(1, "COACH-SERVE: RED\n" as *u8, 17); return 1
188}