@gobing-ai/knowledge-kit 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -14643,6 +14643,75 @@ var init_zod = __esm(() => {
14643
14643
  init_external();
14644
14644
  });
14645
14645
 
14646
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.39/node_modules/@gobing-ai/ts-utils/dist/errors.js
14647
+ var ErrorCode;
14648
+ var init_errors3 = __esm(() => {
14649
+ ErrorCode = {
14650
+ NotFound: "NOT_FOUND",
14651
+ Validation: "VALIDATION",
14652
+ Conflict: "CONFLICT",
14653
+ Internal: "INTERNAL"
14654
+ };
14655
+ });
14656
+
14657
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.39/node_modules/@gobing-ai/ts-utils/dist/api-response.js
14658
+ var API_ERROR_CODES, ERROR_CODE_TO_HTTP, CLIENT_SAFE_CODES;
14659
+ var init_api_response = __esm(() => {
14660
+ init_errors3();
14661
+ API_ERROR_CODES = {
14662
+ SUCCESS: 0,
14663
+ NOT_FOUND: 404,
14664
+ VALIDATION_ERROR: 422,
14665
+ BAD_REQUEST: 400,
14666
+ UNAUTHORIZED: 401,
14667
+ FORBIDDEN: 403,
14668
+ CONFLICT: 409,
14669
+ INTERNAL_ERROR: 500
14670
+ };
14671
+ ERROR_CODE_TO_HTTP = {
14672
+ [ErrorCode.NotFound]: API_ERROR_CODES.NOT_FOUND,
14673
+ [ErrorCode.Validation]: API_ERROR_CODES.VALIDATION_ERROR,
14674
+ [ErrorCode.Conflict]: API_ERROR_CODES.CONFLICT,
14675
+ [ErrorCode.Internal]: API_ERROR_CODES.INTERNAL_ERROR
14676
+ };
14677
+ CLIENT_SAFE_CODES = new Set([ErrorCode.NotFound, ErrorCode.Validation, ErrorCode.Conflict]);
14678
+ });
14679
+
14680
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.39/node_modules/@gobing-ai/ts-utils/dist/cursor.js
14681
+ var init_cursor = () => {};
14682
+
14683
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.39/node_modules/@gobing-ai/ts-utils/dist/event-severity.js
14684
+ var init_event_severity = () => {};
14685
+
14686
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.39/node_modules/@gobing-ai/ts-utils/dist/output.js
14687
+ function processStream(name) {
14688
+ const proc = globalThis.process;
14689
+ const stream = proc?.[name];
14690
+ if (stream === undefined) {
14691
+ throw new Error(`No ${name} target available: set one via setDefaultOutputTargets or pass an explicit target`);
14692
+ }
14693
+ return stream;
14694
+ }
14695
+ function writeLine(message, target) {
14696
+ target.write(`${message}
14697
+ `);
14698
+ }
14699
+ function echo(message, target = defaultStdoutTarget ?? processStream("stdout")) {
14700
+ writeLine(message, target);
14701
+ }
14702
+ function echoError(message, target = defaultStderrTarget ?? processStream("stderr")) {
14703
+ writeLine(message, target);
14704
+ }
14705
+ var defaultStdoutTarget, defaultStderrTarget;
14706
+
14707
+ // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.39/node_modules/@gobing-ai/ts-utils/dist/index.js
14708
+ var init_dist = __esm(() => {
14709
+ init_api_response();
14710
+ init_cursor();
14711
+ init_errors3();
14712
+ init_event_severity();
14713
+ });
14714
+
14646
14715
  // ../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
14647
14716
  var require_identity = __commonJS((exports) => {
14648
14717
  var ALIAS = Symbol.for("yaml.alias");
@@ -17547,15 +17616,15 @@ var require_timestamp = __commonJS((exports) => {
17547
17616
  throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd");
17548
17617
  const [, year, month, day, hour, minute, second] = match.map(Number);
17549
17618
  const millisec = match[7] ? Number((match[7] + "00").substr(1, 3)) : 0;
17550
- let date5 = Date.UTC(year, month - 1, day, hour || 0, minute || 0, second || 0, millisec);
17619
+ let date6 = Date.UTC(year, month - 1, day, hour || 0, minute || 0, second || 0, millisec);
17551
17620
  const tz = match[8];
17552
17621
  if (tz && tz !== "Z") {
17553
17622
  let d = parseSexagesimal(tz, false);
17554
17623
  if (Math.abs(d) < 30)
17555
17624
  d *= 60;
17556
- date5 -= 60000 * d;
17625
+ date6 -= 60000 * d;
17557
17626
  }
17558
- return new Date(date5);
17627
+ return new Date(date6);
17559
17628
  },
17560
17629
  stringify: ({ value }) => value?.toISOString().replace(/(T00:00:00)?\.000Z$/, "") ?? ""
17561
17630
  };
@@ -19415,7 +19484,7 @@ var require_composer = __commonJS((exports) => {
19415
19484
  var node_process = __require("process");
19416
19485
  var directives = require_directives();
19417
19486
  var Document = require_Document();
19418
- var errors3 = require_errors();
19487
+ var errors4 = require_errors();
19419
19488
  var identity = require_identity();
19420
19489
  var composeDoc = require_compose_doc();
19421
19490
  var resolveEnd = require_resolve_end();
@@ -19466,9 +19535,9 @@ var require_composer = __commonJS((exports) => {
19466
19535
  this.onError = (source, code, message, warning) => {
19467
19536
  const pos = getErrorPos(source);
19468
19537
  if (warning)
19469
- this.warnings.push(new errors3.YAMLWarning(pos, code, message));
19538
+ this.warnings.push(new errors4.YAMLWarning(pos, code, message));
19470
19539
  else
19471
- this.errors.push(new errors3.YAMLParseError(pos, code, message));
19540
+ this.errors.push(new errors4.YAMLParseError(pos, code, message));
19472
19541
  };
19473
19542
  this.directives = new directives.Directives({ version: options.version || "1.2" });
19474
19543
  this.options = options;
@@ -19554,7 +19623,7 @@ ${cb}` : comment;
19554
19623
  break;
19555
19624
  case "error": {
19556
19625
  const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message;
19557
- const error51 = new errors3.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg);
19626
+ const error51 = new errors4.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg);
19558
19627
  if (this.atDirectives || !this.doc)
19559
19628
  this.errors.push(error51);
19560
19629
  else
@@ -19564,7 +19633,7 @@ ${cb}` : comment;
19564
19633
  case "doc-end": {
19565
19634
  if (!this.doc) {
19566
19635
  const msg = "Unexpected doc-end without preceding document";
19567
- this.errors.push(new errors3.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg));
19636
+ this.errors.push(new errors4.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg));
19568
19637
  break;
19569
19638
  }
19570
19639
  this.doc.directives.docEnd = true;
@@ -19579,7 +19648,7 @@ ${end.comment}` : end.comment;
19579
19648
  break;
19580
19649
  }
19581
19650
  default:
19582
- this.errors.push(new errors3.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`));
19651
+ this.errors.push(new errors4.YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`));
19583
19652
  }
19584
19653
  }
19585
19654
  *end(forceDoc = false, endOffset = -1) {
@@ -19605,7 +19674,7 @@ ${end.comment}` : end.comment;
19605
19674
  var require_cst_scalar = __commonJS((exports) => {
19606
19675
  var resolveBlockScalar = require_resolve_block_scalar();
19607
19676
  var resolveFlowScalar = require_resolve_flow_scalar();
19608
- var errors3 = require_errors();
19677
+ var errors4 = require_errors();
19609
19678
  var stringifyString = require_stringifyString();
19610
19679
  function resolveAsScalar(token, strict = true, onError) {
19611
19680
  if (token) {
@@ -19614,7 +19683,7 @@ var require_cst_scalar = __commonJS((exports) => {
19614
19683
  if (onError)
19615
19684
  onError(offset, code, message);
19616
19685
  else
19617
- throw new errors3.YAMLParseError([offset, offset + 1], code, message);
19686
+ throw new errors4.YAMLParseError([offset, offset + 1], code, message);
19618
19687
  };
19619
19688
  switch (token.type) {
19620
19689
  case "scalar":
@@ -21494,7 +21563,7 @@ var require_parser = __commonJS((exports) => {
21494
21563
  var require_public_api = __commonJS((exports) => {
21495
21564
  var composer = require_composer();
21496
21565
  var Document = require_Document();
21497
- var errors3 = require_errors();
21566
+ var errors4 = require_errors();
21498
21567
  var log = require_log();
21499
21568
  var identity = require_identity();
21500
21569
  var lineCounter = require_line_counter();
@@ -21511,8 +21580,8 @@ var require_public_api = __commonJS((exports) => {
21511
21580
  const docs = Array.from(composer$1.compose(parser$1.parse(source)));
21512
21581
  if (prettyErrors && lineCounter2)
21513
21582
  for (const doc2 of docs) {
21514
- doc2.errors.forEach(errors3.prettifyError(source, lineCounter2));
21515
- doc2.warnings.forEach(errors3.prettifyError(source, lineCounter2));
21583
+ doc2.errors.forEach(errors4.prettifyError(source, lineCounter2));
21584
+ doc2.warnings.forEach(errors4.prettifyError(source, lineCounter2));
21516
21585
  }
21517
21586
  if (docs.length > 0)
21518
21587
  return docs;
@@ -21527,13 +21596,13 @@ var require_public_api = __commonJS((exports) => {
21527
21596
  if (!doc2)
21528
21597
  doc2 = _doc;
21529
21598
  else if (doc2.options.logLevel !== "silent") {
21530
- doc2.errors.push(new errors3.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
21599
+ doc2.errors.push(new errors4.YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()"));
21531
21600
  break;
21532
21601
  }
21533
21602
  }
21534
21603
  if (prettyErrors && lineCounter2) {
21535
- doc2.errors.forEach(errors3.prettifyError(source, lineCounter2));
21536
- doc2.warnings.forEach(errors3.prettifyError(source, lineCounter2));
21604
+ doc2.errors.forEach(errors4.prettifyError(source, lineCounter2));
21605
+ doc2.warnings.forEach(errors4.prettifyError(source, lineCounter2));
21537
21606
  }
21538
21607
  return doc2;
21539
21608
  }
@@ -21585,12 +21654,12 @@ var require_public_api = __commonJS((exports) => {
21585
21654
  });
21586
21655
 
21587
21656
  // ../../node_modules/.bun/yaml@2.9.0/node_modules/yaml/dist/index.js
21588
- var composer, Document, Schema, errors3, Alias, identity, Pair, Scalar, YAMLMap, YAMLSeq, cst, lexer, lineCounter, parser, publicApi, visit, $Composer, $Document, $Schema, $YAMLError, $YAMLParseError, $YAMLWarning, $Alias, $isAlias, $isCollection, $isDocument, $isMap, $isNode, $isPair, $isScalar, $isSeq, $Pair, $Scalar, $YAMLMap, $YAMLSeq, $Lexer, $LineCounter, $Parser, $parse, $parseAllDocuments, $parseDocument, $stringify, $visit, $visitAsync;
21589
- var init_dist = __esm(() => {
21657
+ var composer, Document, Schema, errors4, Alias, identity, Pair, Scalar, YAMLMap, YAMLSeq, cst, lexer, lineCounter, parser, publicApi, visit, $Composer, $Document, $Schema, $YAMLError, $YAMLParseError, $YAMLWarning, $Alias, $isAlias, $isCollection, $isDocument, $isMap, $isNode, $isPair, $isScalar, $isSeq, $Pair, $Scalar, $YAMLMap, $YAMLSeq, $Lexer, $LineCounter, $Parser, $parse, $parseAllDocuments, $parseDocument, $stringify, $visit, $visitAsync;
21658
+ var init_dist2 = __esm(() => {
21590
21659
  composer = require_composer();
21591
21660
  Document = require_Document();
21592
21661
  Schema = require_Schema();
21593
- errors3 = require_errors();
21662
+ errors4 = require_errors();
21594
21663
  Alias = require_Alias();
21595
21664
  identity = require_identity();
21596
21665
  Pair = require_Pair();
@@ -21606,9 +21675,9 @@ var init_dist = __esm(() => {
21606
21675
  $Composer = composer.Composer;
21607
21676
  $Document = Document.Document;
21608
21677
  $Schema = Schema.Schema;
21609
- $YAMLError = errors3.YAMLError;
21610
- $YAMLParseError = errors3.YAMLParseError;
21611
- $YAMLWarning = errors3.YAMLWarning;
21678
+ $YAMLError = errors4.YAMLError;
21679
+ $YAMLParseError = errors4.YAMLParseError;
21680
+ $YAMLWarning = errors4.YAMLWarning;
21612
21681
  $Alias = Alias.Alias;
21613
21682
  $isAlias = identity.isAlias;
21614
21683
  $isCollection = identity.isCollection;
@@ -21633,10 +21702,10 @@ var init_dist = __esm(() => {
21633
21702
  $visitAsync = visit.visitAsync;
21634
21703
  });
21635
21704
 
21636
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.38/node_modules/@gobing-ai/ts-runtime/dist/config.js
21705
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/config.js
21637
21706
  var configSchema;
21638
21707
  var init_config = __esm(() => {
21639
- init_dist();
21708
+ init_dist2();
21640
21709
  init_zod();
21641
21710
  configSchema = exports_external.object({
21642
21711
  app: exports_external.object({
@@ -21657,7 +21726,7 @@ var init_config = __esm(() => {
21657
21726
  });
21658
21727
  });
21659
21728
 
21660
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.38/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21729
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/file-system-node.js
21661
21730
  import { appendFileSync, cpSync, createReadStream, createWriteStream, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, rmSync, statSync as statSync2, writeFileSync } from "fs";
21662
21731
  import { dirname as dirname2, resolve as resolvePath } from "path";
21663
21732
  function createNodeFileSystem(root, paths) {
@@ -23869,7 +23938,7 @@ var ConfigError = class extends Error {
23869
23938
  }
23870
23939
  };
23871
23940
 
23872
- // ../../node_modules/.bun/@gobing-ai+ts-infra@0.4.35+233bb20824711254/node_modules/@gobing-ai/ts-infra/dist/logger.js
23941
+ // ../../node_modules/.bun/@gobing-ai+ts-infra@0.4.39+6ae14a6e2b3a5898/node_modules/@gobing-ai/ts-infra/dist/logger.js
23873
23942
  var ROOT_CATEGORY = "app";
23874
23943
  function toLogTapeLevel(level) {
23875
23944
  return level === "warn" ? "warning" : level;
@@ -26288,35 +26357,14 @@ function isDirectory(path2) {
26288
26357
  function getByName(result, name) {
26289
26358
  return result.plugins.find((p) => p.name === name);
26290
26359
  }
26291
- // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.38/node_modules/@gobing-ai/ts-runtime/dist/index.js
26360
+ // ../../node_modules/.bun/@gobing-ai+ts-runtime@0.4.39/node_modules/@gobing-ai/ts-runtime/dist/index.js
26292
26361
  init_file_system_node();
26293
26362
  init_config();
26294
26363
 
26295
26364
  // ../../packages/kk-core/src/render-md.ts
26296
26365
  if (false) {}
26297
- // ../../node_modules/.bun/@gobing-ai+ts-utils@0.4.35/node_modules/@gobing-ai/ts-utils/dist/output.js
26298
- var defaultStdoutTarget;
26299
- var defaultStderrTarget;
26300
- function processStream(name) {
26301
- const proc = globalThis.process;
26302
- const stream = proc?.[name];
26303
- if (stream === undefined) {
26304
- throw new Error(`No ${name} target available: set one via setDefaultOutputTargets or pass an explicit target`);
26305
- }
26306
- return stream;
26307
- }
26308
- function writeLine(message, target) {
26309
- target.write(`${message}
26310
- `);
26311
- }
26312
- function echo(message, target = defaultStdoutTarget ?? processStream("stdout")) {
26313
- writeLine(message, target);
26314
- }
26315
- function echoError(message, target = defaultStderrTarget ?? processStream("stderr")) {
26316
- writeLine(message, target);
26317
- }
26318
-
26319
26366
  // src/fanin.ts
26367
+ init_dist();
26320
26368
  var EXIT_USAGE = 2;
26321
26369
  var EXIT_INPUT = 1;
26322
26370
  function mergeDocLists(inputs, sources) {
@@ -26439,11 +26487,13 @@ async function runFanin(argv) {
26439
26487
  // src/fanout.ts
26440
26488
  import { tmpdir } from "os";
26441
26489
  import { isAbsolute as isAbsolute2, join as join3, resolve as resolve4 } from "path";
26490
+ init_dist();
26442
26491
 
26443
26492
  // src/invoke.ts
26444
26493
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
26445
26494
  import { homedir as homedir2 } from "os";
26446
26495
  import { isAbsolute, join as join2, resolve as resolve3 } from "path";
26496
+ init_dist();
26447
26497
  function resolveKkConfigPath() {
26448
26498
  return process.env.KK_CONFIG || join2(homedir2(), ".config/kk/config.yaml");
26449
26499
  }
@@ -27009,8 +27059,10 @@ function registerExecutorCommand(program2) {
27009
27059
  // src/commands/plugin.ts
27010
27060
  import { existsSync as existsSync4 } from "fs";
27011
27061
  import { resolve as resolve5 } from "path";
27062
+ init_dist();
27012
27063
 
27013
27064
  // src/output.ts
27065
+ init_dist();
27014
27066
  function withoutFinalLineEnding(message) {
27015
27067
  return message.replace(/\r?\n$/, "");
27016
27068
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,9 +28,9 @@
28
28
  "typecheck": "tsc --noEmit"
29
29
  },
30
30
  "dependencies": {
31
- "@gobing-ai/ts-infra": "catalog:",
32
- "@gobing-ai/ts-runtime": "catalog:",
33
- "@gobing-ai/ts-utils": "catalog:",
31
+ "@gobing-ai/ts-infra": "^0.4.39",
32
+ "@gobing-ai/ts-runtime": "^0.4.39",
33
+ "@gobing-ai/ts-utils": "^0.4.39",
34
34
  "commander": "15.0.0"
35
35
  },
36
36
  "devDependencies": {
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@gobing-ai/dailynews-gen",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/ts-runtime": "catalog:",
11
+ "@gobing-ai/ts-utils": "catalog:",
12
+ "zod": "4.4.3"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.14"
16
+ }
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "dailynews-gen",
3
+ "kind": "generator",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Generates structured spoken broadcast VoiceScript YAML Content from AI news Doc[]"
7
+ }
@@ -0,0 +1,111 @@
1
+ import { dirname } from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { type Content, ContentSchema, type Doc, DocListSchema } from '@gobing-ai/kk-core';
4
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
5
+ import { echoError } from '@gobing-ai/ts-utils';
6
+ import { buildNewsVoiceScript, type ScriptBuilderOptions } from './script-builder';
7
+
8
+ export * from './script-builder';
9
+
10
+ export interface GeneratorOptions extends ScriptBuilderOptions {
11
+ in: string;
12
+ out: string;
13
+ }
14
+
15
+ export async function processGeneratorIO(
16
+ inputPath: string,
17
+ outputPath: string,
18
+ options?: ScriptBuilderOptions,
19
+ ): Promise<Content> {
20
+ const fs = createNodeFileSystem();
21
+
22
+ let docs: Doc[];
23
+ try {
24
+ const rawInput = await fs.readFile(inputPath);
25
+ docs = DocListSchema.parse(JSON.parse(rawInput));
26
+ } catch (err: unknown) {
27
+ throw new Error(`Invalid DocList input: ${err instanceof Error ? err.message : String(err)}`);
28
+ }
29
+
30
+ const script = buildNewsVoiceScript(docs, options);
31
+ const yamlBody = Bun.YAML.stringify(script);
32
+
33
+ const content: Content = {
34
+ title: script.title,
35
+ body: yamlBody,
36
+ format: 'markdown',
37
+ references: docs
38
+ .filter((d) => d.sourceUri)
39
+ .map((d) => ({
40
+ title: d.title,
41
+ url: d.sourceUri,
42
+ })),
43
+ metadata: {
44
+ generator: 'kk:dailynews-gen',
45
+ language: script.language,
46
+ voiceProfile: script.default_profile,
47
+ storyCount: docs.length,
48
+ stories: docs.map((d) => ({
49
+ id: d.id,
50
+ title: d.title,
51
+ sourceUri: d.sourceUri,
52
+ })),
53
+ },
54
+ };
55
+
56
+ const validatedContent = ContentSchema.parse(content);
57
+
58
+ const outDir = dirname(outputPath);
59
+ if (outDir && outDir !== '.') {
60
+ await fs.ensureDir(outDir);
61
+ }
62
+ await fs.writeFile(outputPath, JSON.stringify(validatedContent, null, 2));
63
+
64
+ return validatedContent;
65
+ }
66
+
67
+ export async function main(): Promise<number> {
68
+ let values: {
69
+ in?: string;
70
+ out?: string;
71
+ language?: string;
72
+ title?: string;
73
+ 'voice-profile'?: string;
74
+ };
75
+
76
+ try {
77
+ ({ values } = parseArgs({
78
+ options: {
79
+ in: { type: 'string' },
80
+ out: { type: 'string' },
81
+ language: { type: 'string' },
82
+ title: { type: 'string' },
83
+ 'voice-profile': { type: 'string' },
84
+ },
85
+ }));
86
+ } catch (err: unknown) {
87
+ echoError(`dailynews-gen failed: ${err instanceof Error ? err.message : String(err)}`);
88
+ return 1;
89
+ }
90
+
91
+ if (!values.in || !values.out) {
92
+ echoError('dailynews-gen failed: Missing required arguments: --in and --out');
93
+ return 1;
94
+ }
95
+
96
+ try {
97
+ await processGeneratorIO(values.in, values.out, {
98
+ language: values.language || process.env.DAILYNEWS_LANGUAGE,
99
+ title: values.title || process.env.DAILYNEWS_TITLE,
100
+ voiceProfile: values['voice-profile'] || process.env.VOICEBOX_DEFAULT_PROFILE,
101
+ });
102
+ return 0;
103
+ } catch (err: unknown) {
104
+ echoError(`dailynews-gen failed: ${err instanceof Error ? err.message : String(err)}`);
105
+ return 1;
106
+ }
107
+ }
108
+
109
+ if (import.meta.main) {
110
+ process.exit(await main());
111
+ }
@@ -0,0 +1,121 @@
1
+ import type { Doc } from '@gobing-ai/kk-core';
2
+ import { z } from 'zod';
3
+
4
+ export const VoiceScriptSchema = z.object({
5
+ title: z.string().min(1),
6
+ language: z.enum(['zh', 'en']),
7
+ default_profile: z.string().min(1).optional(),
8
+ segments: z
9
+ .array(
10
+ z.object({
11
+ text: z.string().min(1),
12
+ gap_ms: z.number().int().nonnegative().optional(),
13
+ instruct: z.string().min(1).optional(),
14
+ }),
15
+ )
16
+ .min(1),
17
+ });
18
+
19
+ export interface VoiceScriptSegment {
20
+ text: string;
21
+ gap_ms?: number;
22
+ instruct?: string;
23
+ }
24
+
25
+ export interface VoiceScript {
26
+ title: string;
27
+ language: string;
28
+ default_profile?: string;
29
+ segments: VoiceScriptSegment[];
30
+ }
31
+
32
+ export interface ScriptBuilderOptions {
33
+ language?: string;
34
+ voiceProfile?: string;
35
+ title?: string;
36
+ }
37
+
38
+ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions): VoiceScript {
39
+ const language = z.enum(['zh', 'en']).parse((options?.language || 'zh').toLowerCase());
40
+ const isZh = language === 'zh';
41
+ const voiceProfile = options?.voiceProfile || process.env.VOICEBOX_DEFAULT_PROFILE || 'Robin';
42
+
43
+ if (docs.length === 0) {
44
+ const title = options?.title || (isZh ? '今日AI快讯(暂无更新)' : 'AI Daily Briefing (No Updates)');
45
+ const segments: VoiceScriptSegment[] = isZh
46
+ ? [
47
+ {
48
+ text: '大家好,欢迎收听今日AI快讯。今天暂无最新的精选新闻动态。',
49
+ gap_ms: 400,
50
+ },
51
+ {
52
+ text: '感谢收听,我们下期再见。',
53
+ gap_ms: 200,
54
+ },
55
+ ]
56
+ : [
57
+ {
58
+ text: 'Hello and welcome to your AI Daily Briefing. There are no new featured stories today.',
59
+ gap_ms: 400,
60
+ },
61
+ {
62
+ text: 'Thank you for listening, and have a great day.',
63
+ gap_ms: 200,
64
+ },
65
+ ];
66
+
67
+ return VoiceScriptSchema.parse({
68
+ title,
69
+ language,
70
+ default_profile: voiceProfile,
71
+ segments,
72
+ });
73
+ }
74
+
75
+ const title =
76
+ options?.title || (isZh ? `今日AI快讯(共${docs.length}条)` : `AI Daily Briefing (${docs.length} stories)`);
77
+ const segments: VoiceScriptSegment[] = [];
78
+
79
+ // 1. Intro segment
80
+ const introText = isZh
81
+ ? `大家好,欢迎收听今日AI快讯。今天为您精选了 ${docs.length} 条重点动态,让我们快速浏览:`
82
+ : `Hello and welcome to your AI Daily Briefing. Here are ${docs.length} top stories for today:`;
83
+
84
+ segments.push({
85
+ text: introText,
86
+ gap_ms: 600,
87
+ });
88
+
89
+ // 2. Story segments
90
+ docs.forEach((doc, idx) => {
91
+ const storyIndex = idx + 1;
92
+ const cleanTitle = (doc.title ?? '').trim();
93
+ const cleanBody = (doc.body ?? '').trim();
94
+
95
+ const storyText = isZh
96
+ ? `第 ${storyIndex} 条动态:${cleanTitle}。${cleanBody}`
97
+ : `Story number ${storyIndex}: ${cleanTitle}. ${cleanBody}`;
98
+
99
+ segments.push({
100
+ text: storyText,
101
+ gap_ms: 500,
102
+ });
103
+ });
104
+
105
+ // 3. Outro segment
106
+ const outroText = isZh
107
+ ? '以上就是今天AI快讯的全部精选内容。感谢您的收听,祝您拥有高效的一天。'
108
+ : 'That concludes our AI Daily Briefing for today. Thanks for listening, and have a productive day.';
109
+
110
+ segments.push({
111
+ text: outroText,
112
+ gap_ms: 300,
113
+ });
114
+
115
+ return VoiceScriptSchema.parse({
116
+ title,
117
+ language,
118
+ default_profile: voiceProfile,
119
+ segments,
120
+ });
121
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -4,6 +4,7 @@ import { type Content, ContentSchema, type Doc, DocListSchema } from '@gobing-ai
4
4
  import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
5
5
  import { echoError } from '@gobing-ai/ts-utils';
6
6
  import { concatWavs } from './concat';
7
+ import { isMp3Requested, type Mp3Transcoder, transcodeWavToMp3 } from './mp3';
7
8
  import { createVoiceboxClient, type VoiceboxClient, type VoiceboxGenerateBody } from './voicebox-client';
8
9
  import {
9
10
  mergeVoiceScripts,
@@ -18,6 +19,7 @@ import {
18
19
  } from './voicescript';
19
20
 
20
21
  export * from './concat';
22
+ export * from './mp3';
21
23
  export * from './voicebox-client';
22
24
  export * from './voicescript';
23
25
 
@@ -35,15 +37,19 @@ export async function processGeneratorIO(
35
37
  inputPath: string,
36
38
  outputPath: string,
37
39
  clientOverride?: VoiceboxClient,
40
+ transcodeMp3?: Mp3Transcoder,
38
41
  ): Promise<void> {
39
42
  const fs = createNodeFileSystem();
40
43
  const outDir = dirname(outputPath);
41
44
  const outStem = basename(outputPath, extname(outputPath));
42
45
  const audioPath = resolve(join(outDir, `${outStem}.wav`));
46
+ const mp3Path = resolve(join(outDir, `${outStem}.mp3`));
47
+ const wantMp3 = isMp3Requested();
43
48
 
44
- // Delete existing output and sibling WAV at start
49
+ // Delete existing output and sibling audio at start
45
50
  await fs.deleteFile(outputPath);
46
51
  await fs.deleteFile(audioPath);
52
+ await fs.deleteFile(mp3Path);
47
53
 
48
54
  let docs: Doc[];
49
55
  try {
@@ -163,18 +169,26 @@ export async function processGeneratorIO(
163
169
  await fs.ensureDir(outDir);
164
170
  await Bun.write(audioPath, concatenatedWav);
165
171
 
172
+ const metadata: Record<string, unknown> = {
173
+ generator: 'kk:voice-gen',
174
+ audioPath,
175
+ duration: totalDuration,
176
+ segments: segmentMetadata,
177
+ };
178
+
179
+ if (wantMp3) {
180
+ const transcode = transcodeMp3 ?? transcodeWavToMp3;
181
+ await transcode(audioPath, mp3Path);
182
+ metadata.mp3Path = mp3Path;
183
+ }
184
+
166
185
  // 6. Build Content
167
186
  const content: Content = {
168
187
  title: combinedScript.title || docs[0]?.title || 'Generated voice',
169
188
  body: Bun.YAML.stringify(combinedScript),
170
189
  format: 'audio',
171
190
  references: [],
172
- metadata: {
173
- generator: 'kk:voice-gen',
174
- audioPath,
175
- duration: totalDuration,
176
- segments: segmentMetadata,
177
- },
191
+ metadata,
178
192
  };
179
193
 
180
194
  const validatedContent = ContentSchema.parse(content);
@@ -182,6 +196,7 @@ export async function processGeneratorIO(
182
196
  } catch (err: unknown) {
183
197
  await fs.deleteFile(outputPath);
184
198
  await fs.deleteFile(audioPath);
199
+ await fs.deleteFile(mp3Path);
185
200
  throw err;
186
201
  }
187
202
  }
@@ -0,0 +1,65 @@
1
+ import { createNodeFileSystem } from '@gobing-ai/ts-runtime';
2
+
3
+ const MP3_TRUTHY = new Set(['1', 'true', 'yes', 'on']);
4
+
5
+ /** True when the operator asked for a sibling MP3 (`VOICE_GEN_MP3=true|1|yes|on`). */
6
+ export function isMp3Requested(env: NodeJS.ProcessEnv = process.env): boolean {
7
+ const raw = env.VOICE_GEN_MP3;
8
+ if (raw === undefined || raw === '') {
9
+ return false;
10
+ }
11
+ return MP3_TRUTHY.has(raw.trim().toLowerCase());
12
+ }
13
+
14
+ /** WAV → MP3. Injected in tests so CI does not need ffmpeg. */
15
+ export type Mp3Transcoder = (wavPath: string, mp3Path: string) => Promise<void>;
16
+
17
+ /**
18
+ * Convert a workspace WAV to MP3 with ffmpeg + libmp3lame (VBR `-qscale:a 2`).
19
+ * Fail-loud if ffmpeg is missing, exits non-zero, or writes no file.
20
+ */
21
+ export async function transcodeWavToMp3(wavPath: string, mp3Path: string, ffmpegBin = 'ffmpeg'): Promise<void> {
22
+ const fs = createNodeFileSystem();
23
+ if (!(await fs.exists(wavPath))) {
24
+ throw new Error(`ffmpeg mp3 transcode failed: WAV input not found: ${wavPath}`);
25
+ }
26
+
27
+ let proc: ReturnType<typeof Bun.spawnSync>;
28
+ try {
29
+ proc = Bun.spawnSync(
30
+ [
31
+ ffmpegBin,
32
+ '-y',
33
+ '-hide_banner',
34
+ '-loglevel',
35
+ 'error',
36
+ '-i',
37
+ wavPath,
38
+ '-codec:a',
39
+ 'libmp3lame',
40
+ '-qscale:a',
41
+ '2',
42
+ mp3Path,
43
+ ],
44
+ { stdout: 'pipe', stderr: 'pipe' },
45
+ );
46
+ } catch (err: unknown) {
47
+ throw new Error(
48
+ `ffmpeg is required for MP3 output (VOICE_GEN_MP3=true) but was not found on PATH (${err instanceof Error ? err.message : String(err)})`,
49
+ );
50
+ }
51
+
52
+ if (proc.exitCode !== 0) {
53
+ const detail = (proc.stderr?.toString() ?? '').trim() || (proc.stdout?.toString() ?? '').trim();
54
+ if (proc.exitCode === null || /not found|ENOENT/i.test(detail)) {
55
+ throw new Error(
56
+ `ffmpeg is required for MP3 output (VOICE_GEN_MP3=true) but was not found on PATH${detail ? `: ${detail}` : ''}`,
57
+ );
58
+ }
59
+ throw new Error(`ffmpeg mp3 transcode failed (exit ${proc.exitCode}): ${detail || 'no stderr'}`);
60
+ }
61
+
62
+ if (!(await fs.exists(mp3Path))) {
63
+ throw new Error(`ffmpeg mp3 transcode produced no file: ${mp3Path}`);
64
+ }
65
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@gobing-ai/aihot-ingest",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/kk-core": "workspace:*",
10
+ "@gobing-ai/ts-runtime": "catalog:",
11
+ "@gobing-ai/ts-utils": "catalog:",
12
+ "zod": "4.4.3"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "1.3.14"
16
+ }
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "aihot-ingest",
3
+ "kind": "ingestion",
4
+ "entry": "./src/index.ts",
5
+ "version": "1.0.0",
6
+ "description": "Ingests curated AI news items from aihot.virxact.com into Doc[]"
7
+ }
@@ -0,0 +1,185 @@
1
+ import { echoError } from '@gobing-ai/ts-utils';
2
+ import { z } from 'zod';
3
+
4
+ export const AIHOT_BASE_URL = 'https://aihot.virxact.com';
5
+ export const DEFAULT_AIHOT_USER_AGENT = 'aihot-ingest/1.0.0';
6
+
7
+ export const AihotItemSchema = z.object({
8
+ id: z.string(),
9
+ title: z.string(),
10
+ originalTitle: z.string().nullable(),
11
+ summary: z.string().nullable(),
12
+ source: z.object({ name: z.string() }),
13
+ links: z.object({
14
+ aihot: z.url(),
15
+ original: z.url(),
16
+ }),
17
+ publishedAt: z.iso.datetime().nullable(),
18
+ discoveredAt: z.iso.datetime(),
19
+ category: z.string().nullable(),
20
+ score: z.number().min(0).max(100).nullable(),
21
+ selected: z.boolean(),
22
+ reason: z.string().nullable().optional(),
23
+ attribution: z
24
+ .object({
25
+ name: z.string(),
26
+ url: z.url(),
27
+ })
28
+ .optional(),
29
+ });
30
+
31
+ export type AihotItem = z.infer<typeof AihotItemSchema>;
32
+
33
+ export const AihotSnapshotResponseSchema = z.object({
34
+ schemaVersion: z.literal(1),
35
+ asOf: z.iso.datetime(),
36
+ fields: z.literal('default'),
37
+ cursor: z.string(),
38
+ count: z.number().int().nonnegative(),
39
+ hasMore: z.boolean(),
40
+ nextPage: z.string().nullable(),
41
+ items: z.array(AihotItemSchema),
42
+ });
43
+
44
+ export type AihotSnapshotResponse = z.infer<typeof AihotSnapshotResponseSchema>;
45
+
46
+ const AihotSnapshotEnvelopeSchema = AihotSnapshotResponseSchema.extend({
47
+ items: z.array(z.unknown()),
48
+ });
49
+
50
+ export const AihotChangeSchema = z.discriminatedUnion('op', [
51
+ z.object({
52
+ op: z.literal('upsert'),
53
+ item: AihotItemSchema,
54
+ changedAt: z.iso.datetime(),
55
+ }),
56
+ z.object({
57
+ op: z.literal('remove'),
58
+ id: z.string(),
59
+ changedAt: z.iso.datetime(),
60
+ }),
61
+ ]);
62
+
63
+ export const AihotChangesResponseSchema = z.object({
64
+ schemaVersion: z.literal(1),
65
+ fields: z.literal('default'),
66
+ cursor: z.string(),
67
+ count: z.number().int().nonnegative(),
68
+ hasMore: z.boolean(),
69
+ changes: z.array(AihotChangeSchema),
70
+ });
71
+
72
+ export type AihotChangesResponse = z.infer<typeof AihotChangesResponseSchema>;
73
+
74
+ const AihotChangesEnvelopeSchema = AihotChangesResponseSchema.extend({
75
+ changes: z.array(z.unknown()),
76
+ });
77
+
78
+ export type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>;
79
+
80
+ export interface AihotDeps {
81
+ fetch: FetchLike;
82
+ baseUrl?: string;
83
+ userAgent?: string;
84
+ warn?: (message: string) => void;
85
+ }
86
+
87
+ export interface FetchSnapshotOptions {
88
+ limit?: number;
89
+ page?: string;
90
+ }
91
+
92
+ export interface FetchChangesOptions {
93
+ cursor: string;
94
+ limit?: number;
95
+ }
96
+
97
+ export class AihotClient {
98
+ private readonly fetch: FetchLike;
99
+ private readonly baseUrl: string;
100
+ private readonly userAgent: string;
101
+ private readonly warn: (message: string) => void;
102
+
103
+ constructor(deps?: Partial<AihotDeps>) {
104
+ this.fetch = deps?.fetch ?? globalThis.fetch;
105
+ this.baseUrl = (deps?.baseUrl ?? AIHOT_BASE_URL).replace(/\/+$/, '');
106
+ this.userAgent = deps?.userAgent ?? DEFAULT_AIHOT_USER_AGENT;
107
+ this.warn = deps?.warn ?? echoError;
108
+ }
109
+
110
+ public async fetchSnapshot(options?: FetchSnapshotOptions): Promise<AihotSnapshotResponse> {
111
+ const url = new URL(`${this.baseUrl}/api/v1/selected/snapshot`);
112
+ if (options?.limit !== undefined) {
113
+ url.searchParams.set('limit', String(options.limit));
114
+ }
115
+ if (options?.page) {
116
+ url.searchParams.set('page', options.page);
117
+ }
118
+ url.searchParams.set('fields', 'default');
119
+
120
+ const res = await this.fetch(url.toString(), {
121
+ method: 'GET',
122
+ headers: {
123
+ Accept: 'application/json',
124
+ 'User-Agent': this.userAgent,
125
+ },
126
+ });
127
+
128
+ if (!res.ok) {
129
+ const errBody = (await res.text()).slice(0, 1000);
130
+ throw new Error(
131
+ `aihot snapshot request failed (HTTP ${res.status} ${res.statusText}): ${errBody || 'Unknown error'}`,
132
+ );
133
+ }
134
+
135
+ const response = AihotSnapshotEnvelopeSchema.parse(await res.json());
136
+ return {
137
+ ...response,
138
+ items: this.parseRecords(response.items, AihotItemSchema, 'snapshot item'),
139
+ };
140
+ }
141
+
142
+ public async fetchChanges(options: FetchChangesOptions): Promise<AihotChangesResponse> {
143
+ const url = new URL(`${this.baseUrl}/api/v1/selected/changes`);
144
+ url.searchParams.set('cursor', options.cursor);
145
+ if (options.limit !== undefined) {
146
+ url.searchParams.set('limit', String(options.limit));
147
+ }
148
+
149
+ const res = await this.fetch(url.toString(), {
150
+ method: 'GET',
151
+ headers: {
152
+ Accept: 'application/json',
153
+ 'User-Agent': this.userAgent,
154
+ },
155
+ });
156
+
157
+ if (!res.ok) {
158
+ const errBody = (await res.text()).slice(0, 1000);
159
+ throw new Error(
160
+ `aihot changes request failed (HTTP ${res.status} ${res.statusText}): ${errBody || 'Unknown error'}`,
161
+ );
162
+ }
163
+
164
+ const response = AihotChangesEnvelopeSchema.parse(await res.json());
165
+ return {
166
+ ...response,
167
+ changes: this.parseRecords(response.changes, AihotChangeSchema, 'change record'),
168
+ };
169
+ }
170
+
171
+ private parseRecords<T>(records: unknown[], schema: z.ZodType<T>, label: string): T[] {
172
+ return records.flatMap((record, index) => {
173
+ const result = schema.safeParse(record);
174
+ if (result.success) return [result.data];
175
+
176
+ const details = result.error.issues.map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`);
177
+ this.warn(`aihot: skipping invalid ${label} at index ${index} (${details.join('; ')})`);
178
+ return [];
179
+ });
180
+ }
181
+ }
182
+
183
+ export function createAihotClient(deps?: Partial<AihotDeps>): AihotClient {
184
+ return new AihotClient(deps);
185
+ }
@@ -0,0 +1,137 @@
1
+ import { dirname } from 'node:path';
2
+ import { parseArgs } from 'node:util';
3
+ import { type Doc, DocListSchema } from '@gobing-ai/kk-core';
4
+ import { atomicWriteJson, createNodeFileSystem, readJsonFile } from '@gobing-ai/ts-runtime';
5
+ import { echoError } from '@gobing-ai/ts-utils';
6
+ import { z } from 'zod';
7
+ import { type AihotDeps, createAihotClient } from './client';
8
+ import { mapAihotItemsToDocs } from './mapper';
9
+
10
+ export * from './client';
11
+ export * from './mapper';
12
+
13
+ export interface IngestionOptions {
14
+ out: string;
15
+ limit?: number;
16
+ cursor?: string;
17
+ stateFile?: string;
18
+ }
19
+
20
+ export interface IngestionResult {
21
+ docs: Doc[];
22
+ cursor: string;
23
+ asOf?: string;
24
+ }
25
+
26
+ const IngestionInputSchema = z.object({
27
+ limit: z.number().int().min(1).max(100).optional(),
28
+ cursor: z.string().trim().min(1).optional(),
29
+ stateFile: z.string().trim().min(1).optional(),
30
+ });
31
+
32
+ const CursorStateSchema = z.object({ cursor: z.string().trim().min(1) });
33
+
34
+ function parseLimit(value: string): number {
35
+ return z.coerce.number().int().min(1).max(100).parse(value);
36
+ }
37
+
38
+ export async function processIngestionIO(
39
+ options: IngestionOptions,
40
+ depsOverride?: Partial<AihotDeps>,
41
+ ): Promise<IngestionResult> {
42
+ const fs = createNodeFileSystem();
43
+ const client = createAihotClient(depsOverride);
44
+
45
+ let effectiveCursor = options.cursor?.trim();
46
+ if (!effectiveCursor && options.stateFile && (await fs.exists(options.stateFile))) {
47
+ effectiveCursor = CursorStateSchema.parse(await readJsonFile(options.stateFile, fs)).cursor;
48
+ }
49
+
50
+ let items: import('./client').AihotItem[];
51
+ let cursor: string;
52
+ let asOf: string | undefined;
53
+
54
+ if (effectiveCursor) {
55
+ const changes = await client.fetchChanges({
56
+ cursor: effectiveCursor,
57
+ limit: options.limit,
58
+ });
59
+ items = changes.changes.flatMap((change) => (change.op === 'upsert' ? [change.item] : []));
60
+ cursor = changes.cursor;
61
+ } else {
62
+ const snapshot = await client.fetchSnapshot({
63
+ limit: options.limit,
64
+ });
65
+ items = snapshot.items;
66
+ cursor = snapshot.cursor;
67
+ asOf = snapshot.asOf;
68
+ }
69
+
70
+ const docs = DocListSchema.parse(mapAihotItemsToDocs(items));
71
+
72
+ const outDir = dirname(options.out);
73
+ if (outDir && outDir !== '.') {
74
+ await fs.ensureDir(outDir);
75
+ }
76
+ await atomicWriteJson(options.out, docs, fs);
77
+
78
+ if (options.stateFile) {
79
+ const stateDir = dirname(options.stateFile);
80
+ if (stateDir && stateDir !== '.') {
81
+ await fs.ensureDir(stateDir);
82
+ }
83
+ const statePayload = {
84
+ cursor,
85
+ asOf,
86
+ lastSyncAt: new Date().toISOString(),
87
+ itemCount: docs.length,
88
+ };
89
+ await atomicWriteJson(options.stateFile, statePayload, fs);
90
+ }
91
+
92
+ return { docs, cursor, asOf };
93
+ }
94
+
95
+ export async function main(depsOverride?: Partial<AihotDeps>): Promise<number> {
96
+ let values: { in?: string; out?: string; limit?: string; cursor?: string; 'state-file'?: string };
97
+ try {
98
+ ({ values } = parseArgs({
99
+ options: {
100
+ in: { type: 'string' },
101
+ out: { type: 'string' },
102
+ limit: { type: 'string' },
103
+ cursor: { type: 'string' },
104
+ 'state-file': { type: 'string' },
105
+ },
106
+ }));
107
+ } catch (err: unknown) {
108
+ echoError(`aihot-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
109
+ return 1;
110
+ }
111
+
112
+ if (!values.out) {
113
+ echoError('aihot-ingest failed: Missing required argument: --out');
114
+ return 1;
115
+ }
116
+
117
+ try {
118
+ const input = values.in ? IngestionInputSchema.parse(await readJsonFile(values.in)) : {};
119
+ await processIngestionIO(
120
+ {
121
+ out: values.out,
122
+ limit: values.limit ? parseLimit(values.limit) : (input.limit ?? 10),
123
+ cursor: values.cursor ?? input.cursor,
124
+ stateFile: values['state-file'] ?? input.stateFile,
125
+ },
126
+ depsOverride,
127
+ );
128
+ return 0;
129
+ } catch (err: unknown) {
130
+ echoError(`aihot-ingest failed: ${err instanceof Error ? err.message : String(err)}`);
131
+ return 1;
132
+ }
133
+ }
134
+
135
+ if (import.meta.main) {
136
+ process.exit(await main());
137
+ }
@@ -0,0 +1,42 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { Doc } from '@gobing-ai/kk-core';
3
+ import type { AihotItem } from './client';
4
+
5
+ export function computeDocId(seed: string): string {
6
+ return createHash('sha256').update(seed).digest('hex').slice(0, 16);
7
+ }
8
+
9
+ export function mapAihotItemToDoc(item: AihotItem): Doc {
10
+ const rawId = item.id.trim();
11
+ const canonicalSource = item.links.original || item.links.aihot || item.title || 'untitled';
12
+ const id = rawId.length > 0 ? rawId : computeDocId(canonicalSource);
13
+
14
+ const sourceUri = item.links?.original || item.links?.aihot || `https://aihot.virxact.com/items/${id}`;
15
+ const title = item.title.trim() || 'Untitled AI News';
16
+ const body = `${item.summary?.trim() || item.title.trim()}\n`;
17
+
18
+ const metadata: Record<string, unknown> = {
19
+ sourceName: item.source.name,
20
+ publishedAt: item.publishedAt,
21
+ discoveredAt: item.discoveredAt,
22
+ category: item.category ?? undefined,
23
+ score: item.score ?? undefined,
24
+ aihotUrl: item.links.aihot,
25
+ originalUrl: item.links.original,
26
+ attribution: item.attribution,
27
+ reason: item.reason ?? undefined,
28
+ };
29
+
30
+ return {
31
+ id,
32
+ title,
33
+ sourceUri,
34
+ body,
35
+ mediaType: 'text/markdown',
36
+ metadata,
37
+ };
38
+ }
39
+
40
+ export function mapAihotItemsToDocs(items: AihotItem[]): Doc[] {
41
+ return items.map(mapAihotItemToDoc);
42
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "include": ["src", "tests"]
4
+ }
@@ -356,7 +356,7 @@ export function parseInput(raw: string): InInput {
356
356
  return { topic: input.topic, maxResults, fixture: input.fixture === true };
357
357
  }
358
358
 
359
- export async function main(): Promise<number> {
359
+ export async function main(depsOverride?: Partial<SearchDeps>): Promise<number> {
360
360
  const fs = createNodeFileSystem();
361
361
  const { values } = parseArgs({
362
362
  options: {
@@ -385,6 +385,7 @@ export async function main(): Promise<number> {
385
385
  fetch: globalThis.fetch,
386
386
  env: process.env,
387
387
  now: () => new Date(),
388
+ ...depsOverride,
388
389
  };
389
390
  // Bun.which with an explicit PATH — without the option it can ignore runtime
390
391
  // process.env.PATH changes (observed on Bun 1.3.14).
@@ -0,0 +1,144 @@
1
+ ---
2
+ "$schema": "@gobing-ai/spur/schemas/state-machine-workflow.schema.json"
3
+ name: kk-daily-ai-voice
4
+ kind: state-machine
5
+ description: >-
6
+ Daily AI news broadcast pipeline orchestrating aihot-ingest, dailynews-gen, and voice-gen
7
+ initialState: prepare
8
+ terminalStates:
9
+ - done
10
+ - failed
11
+ failureStates:
12
+ - failed
13
+
14
+ vars:
15
+ work_dir: ""
16
+ limit: "10"
17
+ language: "zh"
18
+ voice_profile: "Robin"
19
+ title: ""
20
+ cursor: ""
21
+ state_file: ""
22
+ skip_review: "false"
23
+ transcode_mp3: "false"
24
+ plugins_path: ""
25
+ __hitlAnswer: ""
26
+
27
+ states:
28
+ - id: prepare
29
+ description: "Create workspace subdirectories and validate parameters"
30
+ onEnter:
31
+ - kind: shell
32
+ options:
33
+ command: >-
34
+ test -n "${vars.work_dir}" || { echo "missing required variable: work_dir" >&2 && exit 1; };
35
+ mkdir -p "${vars.work_dir}" "${vars.work_dir}/1-ingest" "${vars.work_dir}/2-script" "${vars.work_dir}/3-audio";
36
+ bun -e 'await Bun.write(process.argv[1], JSON.stringify({ limit: Number(process.argv[2]), cursor: process.argv[3] || undefined, stateFile: process.argv[4] || undefined }, null, 2))' "${vars.work_dir}/1-ingest/source.json" "${vars.limit}" "${vars.cursor}" "${vars.state_file}"
37
+
38
+ - id: fetch
39
+ description: "Ingest latest curated AI news from aihot.virxact.com"
40
+ onEnter:
41
+ - kind: shell
42
+ options:
43
+ command: >-
44
+ set --;
45
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
46
+ kk executor run aihot-ingest --in "${vars.work_dir}/1-ingest/source.json" --out "${vars.work_dir}/1-ingest/docs.json" "$@"
47
+
48
+ - id: script
49
+ description: "Compile news Doc[] into structured broadcast VoiceScript YAML Content"
50
+ onEnter:
51
+ - kind: shell
52
+ options:
53
+ command: >-
54
+ set --;
55
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
56
+ DAILYNEWS_LANGUAGE="${vars.language}" DAILYNEWS_TITLE="${vars.title}" VOICEBOX_DEFAULT_PROFILE="${vars.voice_profile}" kk executor run dailynews-gen --in "${vars.work_dir}/1-ingest/docs.json" --out "${vars.work_dir}/2-script/content.json" "$@" &&
57
+ bun -e 'await Bun.write(process.argv[2], (await Bun.file(process.argv[1]).json()).body)' "${vars.work_dir}/2-script/content.json" "${vars.work_dir}/2-script/voicescript.yaml"
58
+
59
+ - id: review
60
+ description: "Operator reviews the generated news script before speech synthesis"
61
+ pause: true
62
+ onEnter:
63
+ - kind: hitl.confirm
64
+ options:
65
+ prompt: "Review daily AI news VoiceScript at ${vars.work_dir}/2-script/voicescript.yaml. Proceed to voice generation?"
66
+ var: __hitlAnswer
67
+
68
+ - id: wrap-docs
69
+ description: "Wrap VoiceScript into Doc[] input for voice-gen"
70
+ onEnter:
71
+ - kind: shell
72
+ options:
73
+ command: >-
74
+ bun -e 'const scriptUrl = new URL(process.argv[1], "file://" + process.cwd() + "/"); const scriptPath = decodeURIComponent(scriptUrl.pathname); const doc = { id: new Bun.CryptoHasher("sha256").update(scriptUrl.href).digest("hex").slice(0, 16), title: "Daily AI News VoiceScript", sourceUri: scriptUrl.href, body: await Bun.file(scriptPath).text(), mediaType: "application/yaml", metadata: { voiceProfile: process.argv[3] || process.env.VOICEBOX_DEFAULT_PROFILE || "Robin" } }; await Bun.write(process.argv[2], JSON.stringify([doc], null, 2))' "${vars.work_dir}/2-script/voicescript.yaml" "${vars.work_dir}/3-audio/docs.json" "${vars.voice_profile}"
75
+
76
+ - id: generate
77
+ description: "Invoke voice-gen generator plugin to generate audio"
78
+ onEnter:
79
+ - kind: shell
80
+ options:
81
+ command: >-
82
+ set --;
83
+ if [ -n "${vars.plugins_path}" ]; then set -- --plugins-path "${vars.plugins_path}"; fi;
84
+ VOICE_GEN_MP3="${vars.transcode_mp3}" kk executor run voice-gen --in "${vars.work_dir}/3-audio/docs.json" --out "${vars.work_dir}/3-audio/content.json" "$@"
85
+
86
+ - id: done
87
+ description: "Terminal — Daily AI news voice broadcast generated successfully"
88
+ - id: failed
89
+ description: "Terminal — Pipeline failed"
90
+
91
+ transitions:
92
+ - from: prepare
93
+ to: fetch
94
+ description: "Workspace prepared -> fetch news items"
95
+ guard:
96
+ kind: always
97
+
98
+ - from: fetch
99
+ to: script
100
+ description: "News ingested -> build broadcast script"
101
+ guard:
102
+ kind: always
103
+
104
+ - from: script
105
+ to: wrap-docs
106
+ description: "skip_review enabled -> bypass HITL review"
107
+ guard:
108
+ kind: shell
109
+ options:
110
+ command: 'test "${vars.skip_review}" = "true"'
111
+
112
+ - from: script
113
+ to: review
114
+ description: "skip_review disabled -> pause for operator review"
115
+ guard:
116
+ kind: always
117
+
118
+ - from: review
119
+ to: wrap-docs
120
+ description: "VoiceScript approved -> wrap for audio generation"
121
+ guard:
122
+ kind: shell
123
+ options:
124
+ command: 'test "${vars.__hitlAnswer}" = "yes"'
125
+
126
+ - from: review
127
+ to: failed
128
+ description: "VoiceScript rejected -> abort"
129
+ guard:
130
+ kind: shell
131
+ options:
132
+ command: 'test "${vars.__hitlAnswer}" = "no"'
133
+
134
+ - from: wrap-docs
135
+ to: generate
136
+ description: "Doc[] wrapped -> synthesize audio"
137
+ guard:
138
+ kind: always
139
+
140
+ - from: generate
141
+ to: done
142
+ description: "Audio generated successfully -> complete"
143
+ guard:
144
+ kind: always
@@ -87,7 +87,7 @@ export class PostsurfingFileCliTransport implements PublishTransport {
87
87
 
88
88
  public async publish(payload: PublishTransportPayload): Promise<Result> {
89
89
  const markdown = mapContentToSurfingMarkdown(payload.content);
90
- const tempDir = join(process.cwd(), '.tmp-surfdash');
90
+ const tempDir = join(process.cwd(), '.spur', 'run', '.tmp-surfdash');
91
91
  await createNodeFileSystem().ensureDir(tempDir);
92
92
 
93
93
  const titleSlug = slugify(payload.content.title ?? 'post');