File size: 10,245 Bytes
d4c5ec7
 
1cd0943
 
d4c5ec7
1cd0943
d4c5ec7
 
 
 
 
 
 
 
 
1cd0943
 
d4c5ec7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1cd0943
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d4c5ec7
1cd0943
d4c5ec7
 
 
1cd0943
 
d4c5ec7
 
 
 
 
 
1cd0943
d4c5ec7
 
 
1cd0943
d4c5ec7
 
 
1cd0943
d4c5ec7
 
1cd0943
d4c5ec7
 
 
 
 
 
 
 
 
1cd0943
 
 
 
d4c5ec7
 
 
 
 
 
 
 
 
 
 
 
 
1cd0943
 
d4c5ec7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// 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;
}