From f32d248d7152415de43c4a34760f888a9598eee7 Mon Sep 17 00:00:00 2001 From: Anders Holck Date: Thu, 30 Apr 2026 13:37:23 +0200 Subject: [PATCH] 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 --- .gitignore | 1 + README.md | 1 + main.c | 86 +++++++++++++++++++++++++++++++++--------------------- 3 files changed, 55 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 11a72cf..36f93fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ irc irc.log chartest +.*.swp diff --git a/README.md b/README.md index 75f44de..5b88c2f 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Port defaults to 6667. | `/names [#channel]` | List users in channel | | `/whois ` | WHOIS query | | `/wii ` | Extended WHOIS (queries remote server) | +| `/trans` | Toggle public translation echo on/off | | `/quit [reason]` | Quit (default: "See you later") | | `/raw ` | Send raw IRC command | diff --git a/main.c b/main.c index 4401577..499132a 100644 --- a/main.c +++ b/main.c @@ -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)