Improve AI translation: skip Swedish/English, /trans toggle, better prompt

- All messages sent to LLM (not just non-Latin)
- LLM detects language and returns SKIP for understood languages
- /trans toggles echoing translations publicly to channel
- PM nicks ordered most-recent-first for tab completion
- tool_choice:none to fix vLLM/litellm 400 error
- Updated README with /trans command
This commit is contained in:
2026-04-30 13:37:23 +02:00
parent 2d9d9a0d65
commit f32d248d71
3 changed files with 55 additions and 33 deletions
+1
View File
@@ -2,3 +2,4 @@
irc
irc.log
chartest
.*.swp
+1
View File
@@ -68,6 +68,7 @@ Port defaults to 6667.
| `/names [#channel]` | List users in channel |
| `/whois <nick>` | WHOIS query |
| `/wii <nick>` | Extended WHOIS (queries remote server) |
| `/trans` | Toggle public translation echo on/off |
| `/quit [reason]` | Quit (default: "See you later") |
| `/raw <line>` | Send raw IRC command |
+53 -33
View File
@@ -77,6 +77,7 @@ static struct {
/* Query target (for /q) */
static char query_target[64] = "";
static int translate_public = 0; /* /trans toggle: echo translations to channel */
/* Track nicks who sent us private messages */
#define MAX_PM_NICKS 32
@@ -85,10 +86,22 @@ static int pm_nick_count = 0;
static void pm_nick_add(const char *n)
{
for (int i = 0; i < pm_nick_count; i++)
if (strcasecmp(pm_nicks[i], n) == 0) return;
/* Move to front if already present */
for (int i = 0; i < pm_nick_count; i++) {
if (strcasecmp(pm_nicks[i], n) == 0) {
if (i == 0) return;
char tmp[NICK_LEN];
memcpy(tmp, pm_nicks[i], NICK_LEN);
memmove(pm_nicks[1], pm_nicks[0], i * NICK_LEN);
memcpy(pm_nicks[0], tmp, NICK_LEN);
return;
}
}
/* Insert at front */
if (pm_nick_count < MAX_PM_NICKS)
snprintf(pm_nicks[pm_nick_count++], NICK_LEN, "%s", n);
pm_nick_count++;
memmove(pm_nicks[1], pm_nicks[0], (pm_nick_count - 1) * NICK_LEN);
snprintf(pm_nicks[0], NICK_LEN, "%s", n);
}
/* AI translation config */
@@ -109,6 +122,7 @@ static struct {
int fd; /* read end of pipe from child */
int level; /* window to display in */
pid_t pid;
char target[128]; /* channel/nick to echo translation to */
} translate_pending[MAX_TRANSLATE];
static int translate_count = 0;
@@ -173,30 +187,15 @@ static void ai_config_load(void)
static int needs_translation(const char *text)
{
if (!ai_cfg.enabled) return 0;
int non_latin_count = 0;
for (const unsigned char *p = (const unsigned char *)text; *p; p++) {
if (*p >= 0x80) {
if ((*p & 0xE0) == 0xC0 && (p[1] & 0xC0) == 0x80) {
unsigned int cp = ((*p & 0x1F) << 6) | (p[1] & 0x3F);
if (cp > 0x024F) non_latin_count++;
p++;
} else if ((*p & 0xF0) == 0xE0 && (p[1] & 0xC0) == 0x80 && (p[2] & 0xC0) == 0x80) {
non_latin_count++;
p += 2;
} else if ((*p & 0xF8) == 0xF0 && (p[1] & 0xC0) == 0x80 && (p[2] & 0xC0) == 0x80 && (p[3] & 0xC0) == 0x80) {
non_latin_count++;
p += 3;
} else {
/* Not valid UTF-8 — skip ISO-8859-1 Latin chars (0xC0-0xFF) */
if (*p < 0xC0) non_latin_count++;
}
}
}
/* Require at least 3 non-Latin chars to avoid triggering on stray åäö */
return non_latin_count >= 3;
/* Skip very short messages (nicks, URLs, single words like "ok") */
int words = 0;
for (const char *p = text; *p; p++)
if (*p == ' ') words++;
if (words < 1 && strlen(text) < 6) return 0;
return 1;
}
static void translate_async(const char *text, int level)
static void translate_async(const char *text, int level, const char *target)
{
if (translate_count >= MAX_TRANSLATE) return;
@@ -242,10 +241,10 @@ static void translate_async(const char *text, int level)
char body[2048];
snprintf(body, sizeof(body),
"{\"model\":\"%s\",\"messages\":["
"{\"role\":\"system\",\"content\":\"Translate to %s. Reply with only the translation, nothing else.\"},"
"{\"role\":\"system\",\"content\":\"You are a translator for an IRC chat. The user understands %s. If the message is in any of those languages or a mix of them, respond with exactly SKIP. Only translate messages in other languages to %s. Respond with only the translation or SKIP.\"},"
"{\"role\":\"user\",\"content\":\"%s\"}"
"],\"stream\":false}",
ai_cfg.model, ai_cfg.target_lang, escaped);
"],\"stream\":false,\"tool_choice\":\"none\"}",
ai_cfg.model, ai_cfg.skip_langs, ai_cfg.target_lang, escaped);
char req[4096];
int rlen = snprintf(req, sizeof(req),
@@ -305,6 +304,9 @@ static void translate_async(const char *text, int level)
translate_pending[translate_count].fd = pipefd[0];
translate_pending[translate_count].level = level;
translate_pending[translate_count].pid = pid;
snprintf(translate_pending[translate_count].target,
sizeof(translate_pending[translate_count].target),
"%s", target ? target : "");
translate_count++;
}
@@ -827,12 +829,12 @@ static void handle_line(char *line)
wprintf(WL_MSG, "<%s> %s\n", sender, text);
pm_nick_add(sender);
if (needs_translation(text))
translate_async(text, WL_MSG);
translate_async(text, WL_MSG, NULL);
} else {
int lvl = chan_to_level(target);
wprintf(lvl, "<%s> %s\n", sender, text);
if (needs_translation(text))
translate_async(text, lvl);
translate_async(text, lvl, translate_public ? target : NULL);
}
}
} else if (strcmp(cmd, "NOTICE") == 0 && params) {
@@ -1185,6 +1187,10 @@ static void handle_input(char *line)
irc_send_raw("PRIVMSG %s :\x01" "ACTION %s\x01", query_target, slap);
wprintf(WL_MSG, "* %s %s\n", nick, slap);
}
} else if (strcasecmp(cmd, "trans") == 0) {
translate_public = !translate_public;
wprintf(current_level, "* Translation echo %s\n",
translate_public ? "ON" : "OFF");
} else if (strcasecmp(cmd, "ctcp") == 0 && args) {
char *target = args;
char *ctcp_cmd = strchr(args, ' ');
@@ -1467,9 +1473,23 @@ int main(int argc, char *argv[])
waitpid(translate_pending[ti].pid, NULL, 0);
if (n > 0) {
tbuf[n] = '\0';
/* Display in italic */
wprintf(translate_pending[ti].level,
" \033[3m%s\033[0m\n", tbuf);
/* Skip if LLM says it's already in a skip language */
if (strcasecmp(tbuf, "SKIP") == 0 ||
strncasecmp(tbuf, "SKIP", 4) == 0) {
/* do nothing */
} else {
wprintf(translate_pending[ti].level,
" \033[3m%s\033[0m\n", tbuf);
if (translate_pending[ti].target[0]) {
char pfx[IRC_MAX];
snprintf(pfx, sizeof(pfx), "PRIVMSG %s :",
translate_pending[ti].target);
irc_send_converted(pfx,
(unsigned char *)tbuf, strlen(tbuf));
wprintf(translate_pending[ti].level,
"<%s> %s\n", nick, tbuf);
}
}
}
translate_count--;
if (ti < translate_count)