dosllm / src /g.cpp
HelloSun's picture
Add 小貓中文: Chinese questions and answers on the DOS screen (Big5, Zhuyin); v86 patch of XiaoMao; updated dev package
1cd0943 verified
Raw History Blame Contribute Delete
10.2 kB
// G.EXE - ask a Gradio Space one question, over plain HTTP, from DOS.
//
// G <question words...> (Big5 Chinese works when XIAOMAO.EXE is loaded: type with Ctrl+Space)
// G (reads the question from the hex variables GQ1..GQ4, see below)
// environment: GRADIO_HOST (default tencent-hy3.hf.space)
// GQ1..GQ4 = the question as hex bytes (the web page uses this because it cannot type Zhuyin)
//
// It speaks the documented Gradio HTTP API itself:
// 1) POST /gradio_api/call/chat {"data":[...9 parameters...]} -> {"event_id":"..."}
// 2) GET /gradio_api/call/chat/<event_id> -> server-sent events
// ("generating" frames while the model streams, then one "complete" frame)
// DOS has no TLS, so the page's bridge (gradio-bridge.js) forwards these requests over HTTPS.
#include "dosnet.h"
#define DEFAULT_HOST "tencent-hy3.hf.space"
#define PROMPT_EN "Answer in plain ASCII English in at most 40 words."
#define PROMPT_ZH "Answer in Traditional Chinese (Taiwan usage) in at most 80 characters. Plain text only: no markdown, no emoji, no Simplified Chinese characters."
#define LINE_MAX_LEN 2000
// The emulator's TCP sends 1460-byte segments back to back, ignores the receive window and never
// retransmits, so a segment that does not fit in our buffer is lost for good and the connection hangs.
// Keep the receive buffer several segments large and drain it quickly.
#define RECV_BUF 8192
static char line[LINE_MAX_LEN];
static int lineLen = 0;
static char finalData[LINE_MAX_LEN];
static char evt[16];
static int haveFinal = 0, sawError = 0, frames = 0;
static char statusLine[48];
static int statusDone = 0;
static char resp[1024];
static int respLen = 0;
static void handleLine( void ) {
if ( !statusDone ) { // first line of an HTTP response
strncpy( statusLine, line, sizeof( statusLine ) - 1 );
statusDone = 1;
return;
}
if ( strncmp( line, "event: ", 7 ) == 0 ) {
strncpy( evt, line + 7, sizeof( evt ) - 1 );
evt[sizeof( evt ) - 1] = 0;
if ( strcmp( evt, "generating" ) == 0 ) { frames++; printf( "." ); fflush( stdout ); }
if ( strcmp( evt, "error" ) == 0 ) sawError = 1;
}
else if ( strncmp( line, "data: ", 6 ) == 0 && strcmp( evt, "complete" ) == 0 ) {
strcpy( finalData, line + 6 );
haveFinal = 1;
}
}
static void feed( const uint8_t *p, int n, int sse ) {
for ( int i = 0; i < n; i++ ) {
if ( !sse ) { if ( respLen < (int)sizeof( resp ) - 1 ) resp[respLen++] = (char)p[i]; continue; }
char c = (char)p[i];
if ( c == '\n' ) { line[lineLen] = 0; handleLine( ); lineLen = 0; }
else if ( c != '\r' && lineLen < LINE_MAX_LEN - 1 ) line[lineLen++] = c;
}
}
static uint16_t portCounter = 0;
// One HTTP request/response on a fresh TCP connection. The response is read until the server closes.
static int httpExchange( IpAddr_t addr, const char *req, int sse, uint32_t timeoutSec ) {
TcpSocket *s = TcpSocketMgr::getSocket( );
if ( s->setRecvBuffer( RECV_BUF ) ) { printf( "TCP: no memory for the receive buffer\n" ); TcpSocketMgr::freeSocket( s ); return -1; }
portCounter += 17;
if ( s->connect( (uint16_t)( pseudoPort( ) + portCounter ), addr, 80, 10000 ) != 0 ) {
TcpSocketMgr::freeSocket( s );
printf( "TCP: connect to port 80 failed\n" );
return -1;
}
lineLen = 0; respLen = 0; statusDone = 0; statusLine[0] = 0; evt[0] = 0;
s->send( (uint8_t *)req, (uint16_t)strlen( req ) );
static uint8_t buf[1024];
clockTicks_t start = TIMER_GET_CURRENT( );
int closed = 0, rc = 0;
while ( !CtrlBreakDetected ) {
Utils::processPackets( );
int16_t r = s->recv( buf, sizeof( buf ) );
if ( r > 0 ) { feed( buf, r, sse ); continue; }
if ( closed ) break;
if ( s->isRemoteClosed( ) ) { closed = 1; continue; } // one more pass to drain the buffer
if ( Timer_diff( start, TIMER_GET_CURRENT( ) ) > TIMER_SECS_TO_TICKS( timeoutSec ) ) {
printf( "\nHTTP: timed out after %lu s\n", (unsigned long)timeoutSec ); rc = -2; break;
}
}
s->close( );
TcpSocketMgr::freeSocket( s );
if ( sse && lineLen > 0 ) { line[lineLen] = 0; handleLine( ); }
return rc;
}
// ---- Big5 (the encoding XIAOMAO.EXE displays): lead A1..F9, trail 40..7E or A1..FE ----
static int isLead( unsigned c ) { return c >= 0xA1 && c <= 0xF9; }
static int isTrail( unsigned c ) { return ( c >= 0x40 && c <= 0x7E ) || ( c >= 0xA1 && c <= 0xFE ); }
static int isBig5Pair( const unsigned char *p ) { return isLead( p[0] ) && isTrail( p[1] ); }
// Keep Big5 pairs intact (the trail byte may be 5Ch '\\'); blank out anything that could break the JSON or the DOS shell.
// Returns 1 if the text contains Chinese.
static int sanitize( char *q ) {
unsigned char *p = (unsigned char *)q, *o = p;
int chinese = 0;
while ( *p ) {
if ( isBig5Pair( p ) ) { *o++ = *p++; *o++ = *p++; chinese = 1; continue; }
unsigned char c = *p++;
if ( c < 32 || c > 126 || c == '"' || c == '\\' ) c = ' ';
*o++ = c;
}
*o = 0;
return chinese;
}
static int hexVal( char h ) { return h >= '0' && h <= '9' ? h - '0' : ( h | 32 ) >= 'a' && ( h | 32 ) <= 'f' ? ( h | 32 ) - 'a' + 10 : -1; }
// Append the bytes encoded as hex in environment variable `name` to q (at most room-1 bytes in total).
static void appendHexEnv( const char *name, char *q, int room ) {
const char *h = getenv( name );
int n = (int)strlen( q );
if ( !h ) return;
while ( h[0] && h[1] && n < room - 1 ) {
int hi = hexVal( h[0] ), lo = hexVal( h[1] );
if ( hi < 0 || lo < 0 ) break;
q[n++] = (char)( hi * 16 + lo ); h += 2;
}
q[n] = 0;
}
// Print `A> text` wrapped at 78 columns. A Chinese character is 2 columns wide and may break anywhere;
// ASCII words break at spaces.
static void wrapPrint( const char *s ) {
const unsigned char *p = (const unsigned char *)s;
int col = 3, space = 0;
printf( "A> " );
while ( *p ) {
if ( *p == ' ' ) { space = 1; p++; continue; }
int w; const unsigned char *u = p;
if ( isBig5Pair( p ) ) { w = 2; p += 2; }
else { while ( *p && *p != ' ' && !isBig5Pair( p ) ) p++; w = (int)( p - u ); }
int need = w + ( space && col > 3 ? 1 : 0 );
if ( col + need > 78 ) { printf( "\n " ); col = 3; space = 0; }
if ( space && col > 3 ) { printf( " " ); col++; }
fwrite( u, 1, w, stdout ); col += w; space = 0;
}
printf( "\n" );
}
// Decode the first JSON string that follows `[["` in the SSE data line and print it.
static void printAnswer( const char *data ) {
const unsigned char *p = (const unsigned char *)strstr( data, "[[\"" );
if ( !p ) { printf( "A> (no text in reply: %.60s)\n", data ); return; }
p += 3;
char out[1200]; int n = 0;
while ( *p && n < (int)sizeof( out ) - 3 ) {
if ( isBig5Pair( p ) ) { out[n++] = (char)p[0]; out[n++] = (char)p[1]; p += 2; continue; } // before escape/quote handling
if ( *p == '"' ) break;
if ( *p == '\\' ) {
p++;
if ( *p == 'n' ) out[n++] = ' ';
else if ( *p == 'u' ) {
unsigned v = 0; int k;
for ( k = 1; k <= 4 && p[k]; k++ ) { char h = (char)p[k]; v = v * 16 + ( h <= '9' ? h - '0' : ( h | 32 ) - 'a' + 10 ); }
out[n++] = ( v >= 32 && v < 127 ) ? (char)v : '?';
p += 4;
}
else out[n++] = (char)*p;
if ( *p ) p++;
continue;
}
out[n++] = (char)*p++;
}
out[n] = 0;
wrapPrint( out );
}
int main( int argc, char *argv[] ) {
char question[160]; question[0] = 0;
for ( int i = 1; i < argc; i++ ) {
if ( strlen( question ) + strlen( argv[i] ) + 2 > sizeof( question ) ) break;
if ( i > 1 ) strcat( question, " " );
strcat( question, argv[i] );
}
if ( !question[0] ) { appendHexEnv( "GQ1", question, sizeof( question ) ); appendHexEnv( "GQ2", question, sizeof( question ) );
appendHexEnv( "GQ3", question, sizeof( question ) ); appendHexEnv( "GQ4", question, sizeof( question ) ); }
int chinese = sanitize( question );
if ( !question[0] ) { printf( "Usage: G <question>\n or set GQ1..GQ4 (hex) env GRADIO_HOST (default %s)\n", DEFAULT_HOST ); return 1; }
const char *host = getenv( "GRADIO_HOST" );
if ( !host || !*host ) host = DEFAULT_HOST;
printf( "G.EXE - asking Gradio Space %s\nQ> %s\n", host, question );
if ( netInit( ) ) { printf( "TCP/IP init failed (is the packet driver loaded?)\n" ); return 2; }
IpAddr_t addr;
printf( "DNS: resolving %s ...\n", host );
if ( resolveHost( host, addr ) ) { printf( "DNS: failed\n" ); netShutdown( 3 ); }
printf( "DNS: %d.%d.%d.%d\n", addr[0], addr[1], addr[2], addr[3] );
clockTicks_t t0 = TIMER_GET_CURRENT( );
char body[520], req[820], now[16];
sprintf( body, "{\"data\":[\"%s\",\"%s\",[],\"no_think\",null,0,0,null,\"\"]}", question, chinese ? PROMPT_ZH : PROMPT_EN );
sprintf( req, "POST /gradio_api/call/chat HTTP/1.0\r\nHost: %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s",
host, (int)strlen( body ), body );
stamp( now );
printf( "[%s] POST /gradio_api/call/chat (%d bytes)\n", now, (int)strlen( body ) );
if ( httpExchange( addr, req, 0, 30 ) ) netShutdown( 4 );
resp[respLen] = 0;
printf( "HTTP: %.40s\n", resp );
char *e = strstr( resp, "\"event_id\":\"" );
if ( !e ) { printf( "No event_id in reply:\n%.300s\n", resp ); netShutdown( 5 ); }
e += 12;
char id[64]; int k = 0;
while ( *e && *e != '"' && k < 62 ) id[k++] = *e++;
id[k] = 0;
printf( "event_id: %s\n", id );
sprintf( req, "GET /gradio_api/call/chat/%s HTTP/1.0\r\nHost: %s\r\nAccept: text/event-stream\r\nConnection: close\r\n\r\n", id, host );
stamp( now );
printf( "[%s] GET /gradio_api/call/chat/<event_id> (waiting for the model) ", now );
fflush( stdout );
if ( httpExchange( addr, req, 1, 150 ) ) netShutdown( 6 );
printf( "\n" );
stamp( now );
if ( !haveFinal ) { printf( "[%s] No answer received (%s, events: %s)\n", now, statusLine, sawError ? "error" : "none" ); netShutdown( 7 ); }
unsigned long secs = ( TIMER_GET_CURRENT( ) - t0 ) / 18;
printf( "[%s] %d streaming frames, done in %lu s\n", now, frames, secs );
printAnswer( finalData );
netShutdown( 0 );
return 0;
}