@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 +102 -50
- package/package.json +4 -4
- package/plugins/generations/dailynews-gen/package.json +17 -0
- package/plugins/generations/dailynews-gen/plugin.json +7 -0
- package/plugins/generations/dailynews-gen/src/index.ts +111 -0
- package/plugins/generations/dailynews-gen/src/script-builder.ts +121 -0
- package/plugins/generations/dailynews-gen/tsconfig.json +4 -0
- package/plugins/generations/voice-gen/src/index.ts +22 -7
- package/plugins/generations/voice-gen/src/mp3.ts +65 -0
- package/plugins/ingestions/aihot-ingest/package.json +17 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +7 -0
- package/plugins/ingestions/aihot-ingest/src/client.ts +185 -0
- package/plugins/ingestions/aihot-ingest/src/index.ts +137 -0
- package/plugins/ingestions/aihot-ingest/src/mapper.ts +42 -0
- package/plugins/ingestions/aihot-ingest/tsconfig.json +4 -0
- package/plugins/ingestions/web-search/src/index.ts +2 -1
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +144 -0
- package/plugins/publishings/surfdash-pub/src/index.ts +1 -1
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
|
|
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
|
-
|
|
17625
|
+
date6 -= 60000 * d;
|
|
17557
17626
|
}
|
|
17558
|
-
return new Date(
|
|
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
|
|
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
|
|
19538
|
+
this.warnings.push(new errors4.YAMLWarning(pos, code, message));
|
|
19470
19539
|
else
|
|
19471
|
-
this.errors.push(new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
21515
|
-
doc2.warnings.forEach(
|
|
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
|
|
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(
|
|
21536
|
-
doc2.warnings.forEach(
|
|
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,
|
|
21589
|
-
var
|
|
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
|
-
|
|
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 =
|
|
21610
|
-
$YAMLParseError =
|
|
21611
|
-
$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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
32
|
-
"@gobing-ai/ts-runtime": "
|
|
33
|
-
"@gobing-ai/ts-utils": "
|
|
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,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
|
+
}
|
|
@@ -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
|
|
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,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
|
+
}
|
|
@@ -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');
|