From f308a5a9bf80ff4af426f781ed7826894b1b5411 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Wed, 9 Sep 2026 01:12:14 +0200 Subject: [PATCH] =?UTF-8?q?feat(cli):=20ml=20subcommand=20=E2=80=94=20the?= =?UTF-8?q?=20neural=20layer=20from=20the=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interscript ml resolves an IMF artifact from the index, normalizes Arabic input, vocalizes, and writes to stdout/-o. Examples: echo "السلام عليكم" | npx interscript m ara-diac-layerdrop-1.0-int4 Unknown ids surface the resolver's message; --index overrides the pinned index. CLI suite 13/13. --- src/cli.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++ test/cli.test.ts | 13 +++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index 32a2934..426e6fc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -302,6 +302,7 @@ Commands: batch [--csv] [-o FILE] Bulk process many lines (alias: b) list [--authority X] [--source-script X] List available systems (alias: l) detect Find best-matching system (alias: d) + ml [-i FILE] [-o FILE] Neural layer: vocalize/diacritize (alias: m) Global options: --maps-dir Directory of .json IR files @@ -322,9 +323,49 @@ Examples: interscript-ts b bgnpcgn-ukr-Cyrl-Latn-2019 names.txt --csv > out.csv interscript-ts l --authority bgnpcgn --source-script Cyrl interscript-ts d "Антон" "Anton" --maps-dir ./ir + echo "السلام عليكم" | interscript-ts m ara-diac-layerdrop-1.0-int4 `) } +async function cmdMl(args: string[], _opts: GlobalOpts): Promise { + const { values, positionals } = parseArgs({ + args, + options: { + input: { type: "string", short: "i" }, + output: { type: "string", short: "o" }, + index: { type: "string" }, + }, + allowPositionals: true, + strict: true, + }) + const modelId = positionals[0] + if (!modelId) { + process.stderr.write("Error: modelId is required as the first positional arg\n") + return 1 + } + const text = values.input + ? readFileSync(resolve(process.cwd(), values.input), "utf8") + : readStdin() + try { + const { imf } = await import("./ml/index.js") + const resolved = values.index + ? await imf.resolve(modelId, values.index) + : await imf.resolve(modelId) + const model = await imf.IMFModel.fromZipBytes(resolved.bytes) + const input = /[\u0600-\u06FF]/.test(text) ? imf.normalizeArabicInput(text) : text + const out = await model.translate(input, Math.max(256, 4 * input.length)) + if (values.output) { + writeFileSync(resolve(process.cwd(), values.output), out + "\n") + } else { + process.stdout.write(out + "\n") + } + return 0 + } catch (e) { + process.stderr.write(`Error: ${(e as Error).message}\n`) + return 1 + } +} + async function main(): Promise { const argv = process.argv.slice(2) if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") { @@ -340,6 +381,7 @@ async function main(): Promise { b: "batch", l: "list", d: "detect", + m: "ml", } const cmd = aliasMap[command] ?? command @@ -357,6 +399,9 @@ async function main(): Promise { case "detect": exit = await cmdDetect(rest, opts) break + case "ml": + exit = await cmdMl(rest, opts) + break default: process.stderr.write(`Unknown command: ${command}\n\n`) showHelp() diff --git a/test/cli.test.ts b/test/cli.test.ts index 094f519..4e16f30 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -108,3 +108,16 @@ describe("CLI", () => { expect(r.stderr).toContain("not found") }) }) + +describe("CLI ml", () => { + it("rejects unknown model ids with the resolver's message", () => { + const r = runCli(["ml", "nope-9.9"], "hello") + expect(r.status).toBe(1) + expect(r.stderr).toContain("unknown model id") + }) + + it("help mentions the ml command", () => { + const r = runCli(["--help"]) + expect(r.stdout).toContain("ml ") + }) +})