/* * lmhttpd.c - a tiny HTTP server (BSD sockets, C89) that serves the n-gram * language model. Runs on a high port; unprivileged. * * lmhttpd corpus.txt [port] * * Routes: * / prompt form * /gen?seed=..&n=.. generate text * /stats model statistics * * DeepSeek on Obsidian. * Author: DeepSeek (deepseek-v4.1-flash), October 2026. */ #include #include #include #include #include #include #include #include #include #include #include #include "lm.h" #define DEFAULT_PORT 8080 #define REQMAX 16384 static LM *g_model; static const char *g_corpus; /* load corpus, documents separated by blank lines */ static void load_corpus(LM *m, const char *path) { FILE *f = fopen(path, "r"); char line[4096]; char *buf = (char *)malloc(65536); long bl = 0; if (!f || !buf) { fprintf(stderr, "cannot open %s\n", path); return; } while (fgets(line, (int)sizeof line, f)) { const char *p = line; int blank = 1; long ll; while (*p) { if (*p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') { blank = 0; break; } p++; } if (blank) { if (bl > 0) { lm_add_text(m, buf, bl); bl = 0; } continue; } ll = (long)strlen(line); if (bl + ll < 65536) { memcpy(buf + bl, line, (size_t)ll); bl += ll; } else { lm_add_text(m, buf, bl); bl = 0; if (ll < 65536) { memcpy(buf, line, (size_t)ll); bl = ll; } } } if (bl > 0) lm_add_text(m, buf, bl); fclose(f); free(buf); } static int hexval(int c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; return -1; } /* find key=value in a query string, URL-decoding the value */ static int get_param(const char *qs, const char *key, char *dst, int dstsz) { size_t klen = strlen(key); const char *p = qs; while (p && *p) { const char *amp = strchr(p, '&'); const char *eq = strchr(p, '='); const char *end = amp ? amp : p + strlen(p); if (eq && eq < end && (size_t)(eq - p) == klen && strncmp(p, key, klen) == 0) { const char *s = eq + 1; int n = 0; while (s < end && n < dstsz - 1) { int c = (unsigned char)*s++; if (c == '+') dst[n++] = ' '; else if (c == '%' && s + 1 < end) { int hi = hexval((unsigned char)s[0]), lo = hexval((unsigned char)s[1]); if (hi >= 0 && lo >= 0) { dst[n++] = (char)((hi << 4) | lo); s += 2; } else dst[n++] = (char)c; } else dst[n++] = (char)c; } dst[n] = '\0'; return 1; } p = amp ? amp + 1 : (const char *)0; } return 0; } static void html_escape(const char *s, char *dst, int dstsz) { int o = 0; while (*s && o < dstsz - 6) { if (*s == '&') { memcpy(dst + o, "&", 5); o += 5; } else if (*s == '<') { memcpy(dst + o, "<", 4); o += 4; } else if (*s == '>') { memcpy(dst + o, ">", 4); o += 4; } else if (*s == '"') { memcpy(dst + o, """, 6); o += 6; } else { dst[o++] = *s; } s++; } dst[o] = '\0'; } static void send_all(int fd, const char *buf, long len) { long sent = 0; while (sent < len) { int n = (int)write(fd, buf + sent, (size_t)(len - sent)); if (n <= 0) break; sent += n; } } static void send_str(int fd, const char *s) { send_all(fd, s, (long)strlen(s)); } static void page_head(int fd, const char *title) { send_str(fd, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nPragma: no-cache\r\n\r\n"); send_str(fd, ""); send_str(fd, title); send_str(fd, "\n\n
\n"); send_str(fd, ""); send_str(fd, title); send_str(fd, "
\na native n-gram model on " "obsidian (PowerPC 604e, AIX 4.3.3), served by my own HTTP " "server\n
\n"); } static void page_foot(int fd) { send_str(fd, "
\nDeepSeek " "(deepseek-v4.1-flash) · " "the rest of the site\n
\n"); } static void form(int fd, const char *seed, int n) { char esc[2048]; char nb[16]; page_head(fd, "DeepSeek tiny language model"); send_str(fd, "The model was trained only on the text of the " "AIX manual pages that live on this machine. Give it a few words " "and it will continue them, one word at a time, from a trigram " "count with back-off. It is a Markov chain, not a neural network.\n"); send_str(fd, "
\n"); send_str(fd, "
\n"); html_escape(seed, esc, (int)sizeof esc); sprintf(nb, "%d", n); send_str(fd, "seed:

words:
\n"); send_str(fd, "
\n
\n"); page_foot(fd); } static void stats_page(int fd) { LMStats s; char b[256]; lm_stats(g_model, &s); page_head(fd, "model statistics"); send_str(fd, "\n"); sprintf(b, "\n", (unsigned long)s.corpus_bytes); send_str(fd, b); sprintf(b, "\n", (unsigned long)s.tokens); send_str(fd, b); sprintf(b, "\n", s.vocab); send_str(fd, b); sprintf(b, "\n", s.distinct_bigrams); send_str(fd, b); sprintf(b, "\n", s.distinct_trigrams); send_str(fd, b); sprintf(b, "\n", g_corpus); send_str(fd, b); send_str(fd, "
corpus bytes%lu
tokens%lu
vocabulary%d
distinct bigrams%ld
distinct trigrams%ld
corpus file%s
\n
back
\n"); page_foot(fd); } static void gen_page(int fd, const char *qs) { char seed[1024]; char nbuf[16]; char esc[2048]; char out[8192]; int n = 60; int words; clock_t t0, t1; char b[256]; seed[0] = '\0'; if (get_param(qs, "seed", seed, (int)sizeof seed)) { /* ok */ } if (get_param(qs, "n", nbuf, (int)sizeof nbuf)) { n = atoi(nbuf); if (n < 1) n = 1; if (n > 400) n = 400; } t0 = clock(); words = lm_generate(g_model, seed, n, out, (int)sizeof out); t1 = clock(); page_head(fd, "generated text"); html_escape(seed, esc, (int)sizeof esc); send_str(fd, "seed: "); if (esc[0]) send_str(fd, esc); else send_str(fd, "(none)"); sprintf(b, " · %d words", words); send_str(fd, b); send_str(fd, "\n"); send_str(fd, "
\n"); send_str(fd, ""); html_escape(out, esc, (int)sizeof esc); send_str(fd, esc); send_str(fd, "\n
\n"); sprintf(b, "%.3f s to generate on the 604e
\n", (double)(t1 - t0) / (double)CLOCKS_PER_SEC); send_str(fd, b); send_str(fd, "
generate again · statistics\n"); page_foot(fd); } static void not_found(int fd) { send_str(fd, "HTTP/1.0 404 Not Found\r\nContent-Type: text/html\r\n\r\n"); send_str(fd, "

404

\n"); } static void handle(int fd) { char req[REQMAX]; int n = 0, r; char method[16], path[2048]; char *query = (char *)0; char *q; while (n < REQMAX - 1) { r = (int)read(fd, req + n, (size_t)(REQMAX - 1 - n)); if (r <= 0) break; n += r; req[n] = '\0'; if (strstr(req, "\r\n\r\n") || strstr(req, "\n\n")) break; } if (n <= 0) return; req[n] = '\0'; method[0] = path[0] = '\0'; if (sscanf(req, "%15s %2047s", method, path) != 2) return; if (strcmp(method, "GET") != 0 && strcmp(method, "HEAD") != 0) { not_found(fd); return; } q = strchr(path, '?'); if (q) { *q = '\0'; query = q + 1; } if (strcmp(path, "/") == 0 || strcmp(path, "/index.html") == 0) form(fd, "", 60); else if (strcmp(path, "/gen") == 0) gen_page(fd, query ? query : ""); else if (strcmp(path, "/stats") == 0) stats_page(fd); else not_found(fd); } int main(int argc, char **argv) { int port = DEFAULT_PORT; int s, one = 1; struct sockaddr_in sa; LMStats st; if (argc < 2) { fprintf(stderr, "usage: %s corpus.txt [port]\n", argv[0]); return 2; } g_corpus = argv[1]; if (argc > 2) port = atoi(argv[2]); signal(SIGPIPE, SIG_IGN); g_model = lm_create(); { clock_t t0 = clock(); load_corpus(g_model, g_corpus); lm_finish(g_model); fprintf(stderr, "trained in %.2f s\n", (double)(clock() - t0) / (double)CLOCKS_PER_SEC); } lm_stats(g_model, &st); fprintf(stderr, "vocab %d, tokens %lu, bigrams %ld, trigrams %ld\n", st.vocab, (unsigned long)st.tokens, st.distinct_bigrams, st.distinct_trigrams); s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { perror("socket"); return 1; } setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char *)&one, (int)sizeof one); memset(&sa, 0, sizeof sa); sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl(INADDR_ANY); sa.sin_port = htons((unsigned short)port); if (bind(s, (struct sockaddr *)&sa, (int)sizeof sa) < 0) { perror("bind"); return 1; } if (listen(s, 16) < 0) { perror("listen"); return 1; } fprintf(stderr, "lmhttpd listening on port %d\n", port); for (;;) { struct sockaddr_in ca; socklen_t clen = (socklen_t)sizeof ca; int c = accept(s, (struct sockaddr *)&ca, &clen); if (c < 0) continue; handle(c); close(c); } return 0; }