@astro-minimax/cli 0.5.0 → 0.7.1
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/README.md +69 -0
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +99 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/data.d.ts +2 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/data.js +111 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +378 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/podcast.d.ts +2 -0
- package/dist/commands/podcast.d.ts.map +1 -0
- package/dist/commands/podcast.js +89 -0
- package/dist/commands/podcast.js.map +1 -0
- package/dist/commands/post.d.ts +2 -0
- package/dist/commands/post.d.ts.map +1 -0
- package/dist/commands/post.js +190 -0
- package/dist/commands/post.js.map +1 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +88 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/ai-process.d.ts +20 -0
- package/dist/tools/ai-process.d.ts.map +1 -0
- package/dist/tools/ai-process.js +607 -0
- package/dist/tools/ai-process.js.map +1 -0
- package/dist/tools/build-author-context.d.ts +13 -0
- package/dist/tools/build-author-context.d.ts.map +1 -0
- package/dist/tools/build-author-context.js +313 -0
- package/dist/tools/build-author-context.js.map +1 -0
- package/dist/tools/build-voice-profile.d.ts +12 -0
- package/dist/tools/build-voice-profile.d.ts.map +1 -0
- package/dist/tools/build-voice-profile.js +270 -0
- package/dist/tools/build-voice-profile.js.map +1 -0
- package/dist/tools/eval-ai-chat.d.ts +17 -0
- package/dist/tools/eval-ai-chat.d.ts.map +1 -0
- package/dist/tools/eval-ai-chat.js +362 -0
- package/dist/tools/eval-ai-chat.js.map +1 -0
- package/dist/tools/generate-author-profile.d.ts +14 -0
- package/dist/tools/generate-author-profile.d.ts.map +1 -0
- package/dist/tools/generate-author-profile.js +289 -0
- package/dist/tools/generate-author-profile.js.map +1 -0
- package/dist/tools/generate-cover.d.ts +14 -0
- package/dist/tools/generate-cover.d.ts.map +1 -0
- package/dist/tools/generate-cover.js +95 -0
- package/dist/tools/generate-cover.js.map +1 -0
- package/dist/tools/generate-og.d.ts +3 -0
- package/dist/tools/generate-og.d.ts.map +1 -0
- package/dist/tools/generate-og.js +254 -0
- package/dist/tools/generate-og.js.map +1 -0
- package/dist/tools/generate-related.d.ts +11 -0
- package/dist/tools/generate-related.d.ts.map +1 -0
- package/dist/tools/generate-related.js +124 -0
- package/dist/tools/generate-related.js.map +1 -0
- package/dist/tools/generate-tags.d.ts +14 -0
- package/dist/tools/generate-tags.d.ts.map +1 -0
- package/dist/tools/generate-tags.js +182 -0
- package/dist/tools/generate-tags.js.map +1 -0
- package/dist/tools/lib/ai-provider.d.ts +43 -0
- package/dist/tools/lib/ai-provider.d.ts.map +1 -0
- package/dist/tools/lib/ai-provider.js +146 -0
- package/dist/tools/lib/ai-provider.js.map +1 -0
- package/dist/tools/lib/audio-processor.d.ts +46 -0
- package/dist/tools/lib/audio-processor.d.ts.map +1 -0
- package/dist/tools/lib/audio-processor.js +188 -0
- package/dist/tools/lib/audio-processor.js.map +1 -0
- package/dist/tools/lib/frontmatter.d.ts +11 -0
- package/dist/tools/lib/frontmatter.d.ts.map +1 -0
- package/dist/tools/lib/frontmatter.js +80 -0
- package/dist/tools/lib/frontmatter.js.map +1 -0
- package/dist/tools/lib/index.d.ts +7 -0
- package/dist/tools/lib/index.d.ts.map +1 -0
- package/{template/tools/lib/index.ts → dist/tools/lib/index.js} +1 -0
- package/dist/tools/lib/index.js.map +1 -0
- package/dist/tools/lib/markdown.d.ts +6 -0
- package/dist/tools/lib/markdown.d.ts.map +1 -0
- package/dist/tools/lib/markdown.js +34 -0
- package/dist/tools/lib/markdown.js.map +1 -0
- package/dist/tools/lib/posts.d.ts +25 -0
- package/dist/tools/lib/posts.d.ts.map +1 -0
- package/dist/tools/lib/posts.js +63 -0
- package/dist/tools/lib/posts.js.map +1 -0
- package/dist/tools/lib/script-generator.d.ts +61 -0
- package/dist/tools/lib/script-generator.d.ts.map +1 -0
- package/dist/tools/lib/script-generator.js +182 -0
- package/dist/tools/lib/script-generator.js.map +1 -0
- package/dist/tools/lib/tts-provider.d.ts +65 -0
- package/dist/tools/lib/tts-provider.d.ts.map +1 -0
- package/dist/tools/lib/tts-provider.js +116 -0
- package/dist/tools/lib/tts-provider.js.map +1 -0
- package/dist/tools/lib/types.d.ts +129 -0
- package/dist/tools/lib/types.d.ts.map +1 -0
- package/dist/tools/lib/types.js +64 -0
- package/dist/tools/lib/types.js.map +1 -0
- package/dist/tools/lib/utils.d.ts +18 -0
- package/dist/tools/lib/utils.d.ts.map +1 -0
- package/dist/tools/lib/utils.js +121 -0
- package/dist/tools/lib/utils.js.map +1 -0
- package/dist/tools/lib/vectors.d.ts +27 -0
- package/dist/tools/lib/vectors.d.ts.map +1 -0
- package/dist/tools/lib/vectors.js +64 -0
- package/dist/tools/lib/vectors.js.map +1 -0
- package/dist/tools/podcast-feed.d.ts +6 -0
- package/dist/tools/podcast-feed.d.ts.map +1 -0
- package/dist/tools/podcast-feed.js +121 -0
- package/dist/tools/podcast-feed.js.map +1 -0
- package/dist/tools/podcast-generate.d.ts +15 -0
- package/dist/tools/podcast-generate.d.ts.map +1 -0
- package/dist/tools/podcast-generate.js +318 -0
- package/dist/tools/podcast-generate.js.map +1 -0
- package/dist/tools/podcast-list.d.ts +6 -0
- package/dist/tools/podcast-list.d.ts.map +1 -0
- package/dist/tools/podcast-list.js +66 -0
- package/dist/tools/podcast-list.js.map +1 -0
- package/dist/tools/summarize.d.ts +16 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +108 -0
- package/dist/tools/summarize.js.map +1 -0
- package/dist/tools/translate.d.ts +13 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +46 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/vectorize.d.ts +13 -0
- package/dist/tools/vectorize.d.ts.map +1 -0
- package/dist/tools/vectorize.js +87 -0
- package/dist/tools/vectorize.js.map +1 -0
- package/package.json +14 -9
- package/template/astro.config.ts +8 -28
- package/template/datas/ai-seo.json +8 -0
- package/template/datas/ai-skip-list.json +1 -0
- package/template/datas/author-profile-context.json +21 -0
- package/template/datas/author-profile-report.json +21 -0
- package/template/datas/eval/gold-set.json +72 -0
- package/template/functions/README.md +82 -0
- package/template/functions/api/ai-info.ts +2 -2
- package/template/functions/api/chat.ts +4 -1
- package/template/functions/api/notify/comment.ts +140 -68
- package/template/functions/api/notify/debug.ts +41 -0
- package/template/functions/api/notify/status.ts +97 -0
- package/template/functions/api/notify/test-ai-chat.ts +67 -0
- package/template/package.json +22 -25
- package/template/src/config.ts +11 -0
- package/template/src/content.config.ts +29 -16
- package/template/src/env.d.ts +0 -5
- package/index.js +0 -36
- package/template/tools/README.md +0 -169
- package/template/tools/ai-process.ts +0 -816
- package/template/tools/build-author-context.ts +0 -405
- package/template/tools/build-voice-profile.ts +0 -322
- package/template/tools/generate-author-profile.ts +0 -369
- package/template/tools/generate-cover.ts +0 -123
- package/template/tools/generate-og.ts +0 -280
- package/template/tools/generate-related.ts +0 -146
- package/template/tools/generate-tags.ts +0 -251
- package/template/tools/lib/ai-provider.ts +0 -240
- package/template/tools/lib/frontmatter.ts +0 -94
- package/template/tools/lib/markdown.ts +0 -40
- package/template/tools/lib/posts.ts +0 -89
- package/template/tools/lib/utils.ts +0 -138
- package/template/tools/lib/vectors.ts +0 -96
- package/template/tools/summarize.ts +0 -142
- package/template/tools/translate.ts +0 -60
- package/template/tools/vectorize.ts +0 -105
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posts.d.ts","sourceRoot":"","sources":["../../../src/tools/lib/posts.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH,eAAO,MAAM,SAAS,QAAuC,CAAC;AAE9D,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAK7C;AAED,wBAAsB,WAAW,CAAC,IAAI,CAAC,EAAE;IACvC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAqCtB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG;IACtD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAaA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 博客文章读取与遍历工具
|
|
3
|
+
*/
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { extractFrontmatter } from "./frontmatter.js";
|
|
7
|
+
import { stripMarkdown } from "./markdown.js";
|
|
8
|
+
export const BLOG_PATH = join(process.cwd(), "src/data/blog");
|
|
9
|
+
export function getPostURL(id) {
|
|
10
|
+
const parts = id.split("/");
|
|
11
|
+
const lang = parts[0];
|
|
12
|
+
const slug = parts.slice(1).join("/");
|
|
13
|
+
return `/${lang}/posts/${slug}/`;
|
|
14
|
+
}
|
|
15
|
+
export async function getAllPosts(opts) {
|
|
16
|
+
const posts = [];
|
|
17
|
+
async function walk(dir) {
|
|
18
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const fullPath = join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory() && !entry.name.startsWith("_")) {
|
|
22
|
+
await walk(fullPath);
|
|
23
|
+
}
|
|
24
|
+
else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) {
|
|
25
|
+
const content = await readFile(fullPath, "utf-8");
|
|
26
|
+
const fm = extractFrontmatter(content);
|
|
27
|
+
const isDraft = fm.data.draft === true;
|
|
28
|
+
if (isDraft && !opts?.includeDrafts)
|
|
29
|
+
continue;
|
|
30
|
+
const relativePath = fullPath.replace(BLOG_PATH + "/", "");
|
|
31
|
+
const lang = relativePath.startsWith("en/") ? "en" : "zh";
|
|
32
|
+
const id = relativePath.replace(/\.(md|mdx)$/, "");
|
|
33
|
+
posts.push({
|
|
34
|
+
id,
|
|
35
|
+
filePath: fullPath,
|
|
36
|
+
lang,
|
|
37
|
+
title: fm.data.title || "",
|
|
38
|
+
description: fm.data.description || "",
|
|
39
|
+
tags: Array.isArray(fm.data.tags) ? fm.data.tags : [],
|
|
40
|
+
category: fm.data.category || "",
|
|
41
|
+
body: opts?.stripBody === false ? fm.body : stripMarkdown(content),
|
|
42
|
+
draft: isDraft,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await walk(BLOG_PATH);
|
|
48
|
+
return posts;
|
|
49
|
+
}
|
|
50
|
+
export function getExistingTaxonomy(posts) {
|
|
51
|
+
const tagSet = new Set();
|
|
52
|
+
const catSet = new Set();
|
|
53
|
+
for (const post of posts) {
|
|
54
|
+
post.tags.forEach(t => tagSet.add(t));
|
|
55
|
+
if (post.category)
|
|
56
|
+
catSet.add(post.category);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
tags: Array.from(tagSet).sort(),
|
|
60
|
+
categories: Array.from(catSet).sort(),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=posts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posts.js","sourceRoot":"","sources":["../../../src/tools/lib/posts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script generator module for podcast generation.
|
|
3
|
+
* Converts blog posts to podcast dialogue scripts using LLM.
|
|
4
|
+
*
|
|
5
|
+
* @module podcast/script-generator
|
|
6
|
+
*/
|
|
7
|
+
import type { PodcastScript, PodcastSpeaker, DialogueSegment } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Options for script generation.
|
|
10
|
+
*/
|
|
11
|
+
export interface ScriptGeneratorOptions {
|
|
12
|
+
/** Maximum characters per dialogue segment (default: 200) */
|
|
13
|
+
maxSegmentLength?: number;
|
|
14
|
+
/** Custom speaker configuration */
|
|
15
|
+
speakers?: PodcastSpeaker[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate a podcast script from a blog article.
|
|
19
|
+
*
|
|
20
|
+
* @param article - Article data including id, title, body, etc.
|
|
21
|
+
* @param options - Generation options
|
|
22
|
+
* @returns Generated podcast script
|
|
23
|
+
* @throws Error if LLM generation or parsing fails
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const script = await generateScript({
|
|
28
|
+
* id: "zh/getting-started",
|
|
29
|
+
* title: "快速开始",
|
|
30
|
+
* lang: "zh",
|
|
31
|
+
* body: "文章内容...",
|
|
32
|
+
* category: "教程",
|
|
33
|
+
* tags: ["astro", "blog"],
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateScript(article: {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
lang: "zh" | "en";
|
|
41
|
+
body: string;
|
|
42
|
+
category: string;
|
|
43
|
+
tags: string[];
|
|
44
|
+
}, options?: ScriptGeneratorOptions): Promise<PodcastScript>;
|
|
45
|
+
/**
|
|
46
|
+
* Calculate the total character count of a script.
|
|
47
|
+
* @param segments - Dialogue segments
|
|
48
|
+
* @returns Total character count
|
|
49
|
+
*/
|
|
50
|
+
export declare function calculateScriptLength(segments: DialogueSegment[]): number;
|
|
51
|
+
/**
|
|
52
|
+
* Estimate the duration of a podcast script.
|
|
53
|
+
* Assumes average speaking rate of 3 characters per second for Chinese,
|
|
54
|
+
* 15 characters per second for English.
|
|
55
|
+
*
|
|
56
|
+
* @param segments - Dialogue segments
|
|
57
|
+
* @param lang - Language of the script
|
|
58
|
+
* @returns Estimated duration in seconds
|
|
59
|
+
*/
|
|
60
|
+
export declare function estimateDuration(segments: DialogueSegment[], lang: "zh" | "en"): number;
|
|
61
|
+
//# sourceMappingURL=script-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-generator.d.ts","sourceRoot":"","sources":["../../../src/tools/lib/script-generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,eAAe,EAChB,MAAM,YAAY,CAAC;AAGpB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mCAAmC;IACnC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B;AAmID;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE;IACP,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,EACD,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,aAAa,CAAC,CAoCxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,MAAM,CAEzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,eAAe,EAAE,EAC3B,IAAI,EAAE,IAAI,GAAG,IAAI,GAChB,MAAM,CAIR"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script generator module for podcast generation.
|
|
3
|
+
* Converts blog posts to podcast dialogue scripts using LLM.
|
|
4
|
+
*
|
|
5
|
+
* @module podcast/script-generator
|
|
6
|
+
*/
|
|
7
|
+
import { chatCompletion } from "./ai-provider.js";
|
|
8
|
+
import { DEFAULT_SPEAKERS } from "./types.js";
|
|
9
|
+
/** Default maximum segment length for TTS optimization */
|
|
10
|
+
const DEFAULT_MAX_SEGMENT_LENGTH = 200;
|
|
11
|
+
/**
|
|
12
|
+
* Build the system prompt for the LLM.
|
|
13
|
+
* @param lang - Language of the content
|
|
14
|
+
* @param maxSegmentLength - Maximum characters per segment
|
|
15
|
+
* @returns System prompt string
|
|
16
|
+
*/
|
|
17
|
+
function buildSystemPrompt(lang, maxSegmentLength) {
|
|
18
|
+
if (lang === "en") {
|
|
19
|
+
return `You are a podcast scriptwriter. Convert the given blog post into a natural dialogue between a host and a guest expert.
|
|
20
|
+
|
|
21
|
+
Requirements:
|
|
22
|
+
1. Preserve ALL key technical information and concepts
|
|
23
|
+
2. Create natural conversation flow with transitions
|
|
24
|
+
3. Host asks clarifying questions; Guest provides detailed explanations
|
|
25
|
+
4. Each segment must be under ${maxSegmentLength} characters for TTS optimization
|
|
26
|
+
5. Split long explanations into multiple segments
|
|
27
|
+
6. Include a brief intro and outro
|
|
28
|
+
7. Output ONLY valid JSON, no markdown code blocks
|
|
29
|
+
|
|
30
|
+
Output format:
|
|
31
|
+
{"segments":[{"speaker":"host","text":"..."},{"speaker":"guest","text":"..."}]}
|
|
32
|
+
|
|
33
|
+
Respond in English.`;
|
|
34
|
+
}
|
|
35
|
+
return `你是一位播客脚本撰写专家。将给定的博客文章转换为主持人和嘉宾专家之间的自然对话。
|
|
36
|
+
|
|
37
|
+
要求:
|
|
38
|
+
1. 保留所有关键技术信息和概念
|
|
39
|
+
2. 创建自然的对话流程,包含过渡
|
|
40
|
+
3. 主持人提出澄清性问题,嘉宾提供详细解释
|
|
41
|
+
4. 每个对话片段必须控制在 ${maxSegmentLength} 字符以内(TTS 优化)
|
|
42
|
+
5. 将长解释拆分为多个片段
|
|
43
|
+
6. 包含简短的开场和结束语
|
|
44
|
+
7. 只输出有效的 JSON,不要 markdown 代码块
|
|
45
|
+
|
|
46
|
+
输出格式:
|
|
47
|
+
{"segments":[{"speaker":"host","text":"..."},{"speaker":"guest","text":"..."}]}
|
|
48
|
+
|
|
49
|
+
请使用中文回复。`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the user prompt with article content.
|
|
53
|
+
* @param title - Article title
|
|
54
|
+
* @param content - Article body content
|
|
55
|
+
* @param category - Article category
|
|
56
|
+
* @param tags - Article tags
|
|
57
|
+
* @returns User prompt string
|
|
58
|
+
*/
|
|
59
|
+
function buildUserPrompt(title, content, category, tags) {
|
|
60
|
+
const lang = title.match(/[\u4e00-\u9fa5]/) ? "zh" : "en";
|
|
61
|
+
if (lang === "en") {
|
|
62
|
+
return `Article Title: ${title}
|
|
63
|
+
Category: ${category || "None"}
|
|
64
|
+
Tags: ${tags.join(", ") || "None"}
|
|
65
|
+
|
|
66
|
+
Article Content:
|
|
67
|
+
${content.slice(0, 8000)}`;
|
|
68
|
+
}
|
|
69
|
+
return `文章标题:${title}
|
|
70
|
+
分类:${category || "无"}
|
|
71
|
+
标签:${tags.join("、") || "无"}
|
|
72
|
+
|
|
73
|
+
文章正文:
|
|
74
|
+
${content.slice(0, 8000)}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse the LLM response into dialogue segments.
|
|
78
|
+
* @param raw - Raw LLM response string
|
|
79
|
+
* @returns Array of dialogue segments
|
|
80
|
+
* @throws Error if parsing fails or segments array is missing
|
|
81
|
+
*/
|
|
82
|
+
function parseDialogueResponse(raw) {
|
|
83
|
+
// Try to extract JSON from markdown code blocks if present
|
|
84
|
+
const jsonMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
85
|
+
const jsonStr = (jsonMatch ? jsonMatch[1] : raw).trim();
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(jsonStr);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
throw new Error(`Failed to parse LLM response as JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
92
|
+
}
|
|
93
|
+
if (!parsed.segments || !Array.isArray(parsed.segments)) {
|
|
94
|
+
throw new Error("Invalid response: missing segments array");
|
|
95
|
+
}
|
|
96
|
+
// Validate and clean segments
|
|
97
|
+
const segments = [];
|
|
98
|
+
for (const seg of parsed.segments) {
|
|
99
|
+
const speaker = seg.speaker || "host";
|
|
100
|
+
const text = (seg.text || "").trim();
|
|
101
|
+
// Skip empty segments
|
|
102
|
+
if (text.length === 0)
|
|
103
|
+
continue;
|
|
104
|
+
// Validate speaker
|
|
105
|
+
if (speaker !== "host" && speaker !== "guest") {
|
|
106
|
+
segments.push({ speaker: "host", text });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
segments.push({ speaker, text });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (segments.length === 0) {
|
|
113
|
+
throw new Error("No valid dialogue segments generated");
|
|
114
|
+
}
|
|
115
|
+
return segments;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate a podcast script from a blog article.
|
|
119
|
+
*
|
|
120
|
+
* @param article - Article data including id, title, body, etc.
|
|
121
|
+
* @param options - Generation options
|
|
122
|
+
* @returns Generated podcast script
|
|
123
|
+
* @throws Error if LLM generation or parsing fails
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* const script = await generateScript({
|
|
128
|
+
* id: "zh/getting-started",
|
|
129
|
+
* title: "快速开始",
|
|
130
|
+
* lang: "zh",
|
|
131
|
+
* body: "文章内容...",
|
|
132
|
+
* category: "教程",
|
|
133
|
+
* tags: ["astro", "blog"],
|
|
134
|
+
* });
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export async function generateScript(article, options = {}) {
|
|
138
|
+
const maxSegmentLength = options.maxSegmentLength || DEFAULT_MAX_SEGMENT_LENGTH;
|
|
139
|
+
const speakers = options.speakers || DEFAULT_SPEAKERS[article.lang] || DEFAULT_SPEAKERS.zh;
|
|
140
|
+
// Build prompts
|
|
141
|
+
const systemPrompt = buildSystemPrompt(article.lang, maxSegmentLength);
|
|
142
|
+
const userPrompt = buildUserPrompt(article.title, article.body, article.category, article.tags);
|
|
143
|
+
// Call LLM
|
|
144
|
+
const raw = await chatCompletion([
|
|
145
|
+
{ role: "system", content: systemPrompt },
|
|
146
|
+
{ role: "user", content: userPrompt },
|
|
147
|
+
], { maxTokens: 4096 });
|
|
148
|
+
// Parse response
|
|
149
|
+
const segments = parseDialogueResponse(raw);
|
|
150
|
+
return {
|
|
151
|
+
slug: article.id,
|
|
152
|
+
title: article.title,
|
|
153
|
+
lang: article.lang,
|
|
154
|
+
speakers,
|
|
155
|
+
segments,
|
|
156
|
+
generatedAt: new Date().toISOString(),
|
|
157
|
+
contentHash: "", // Will be set by caller
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Calculate the total character count of a script.
|
|
162
|
+
* @param segments - Dialogue segments
|
|
163
|
+
* @returns Total character count
|
|
164
|
+
*/
|
|
165
|
+
export function calculateScriptLength(segments) {
|
|
166
|
+
return segments.reduce((sum, seg) => sum + seg.text.length, 0);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Estimate the duration of a podcast script.
|
|
170
|
+
* Assumes average speaking rate of 3 characters per second for Chinese,
|
|
171
|
+
* 15 characters per second for English.
|
|
172
|
+
*
|
|
173
|
+
* @param segments - Dialogue segments
|
|
174
|
+
* @param lang - Language of the script
|
|
175
|
+
* @returns Estimated duration in seconds
|
|
176
|
+
*/
|
|
177
|
+
export function estimateDuration(segments, lang) {
|
|
178
|
+
const totalChars = calculateScriptLength(segments);
|
|
179
|
+
const charsPerSecond = lang === "zh" ? 3 : 15;
|
|
180
|
+
return Math.ceil(totalChars / charsPerSecond);
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=script-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-generator.js","sourceRoot":"","sources":["../../../src/tools/lib/script-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS (Text-to-Speech) provider module for podcast generation.
|
|
3
|
+
* Supports OpenAI TTS API with proxy configuration.
|
|
4
|
+
*
|
|
5
|
+
* @module podcast/tts-provider
|
|
6
|
+
*/
|
|
7
|
+
import type { OpenAIVoice } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* TTS configuration resolved from environment variables.
|
|
10
|
+
*/
|
|
11
|
+
export interface TTSConfig {
|
|
12
|
+
/** Provider type (currently only "openai") */
|
|
13
|
+
provider: "openai";
|
|
14
|
+
/** API key for authentication */
|
|
15
|
+
apiKey: string;
|
|
16
|
+
/** Base URL for the API */
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Options for TTS generation.
|
|
21
|
+
*/
|
|
22
|
+
export interface TTSOptions {
|
|
23
|
+
/** Voice to use for synthesis */
|
|
24
|
+
voice: OpenAIVoice;
|
|
25
|
+
/** Model: "tts-1" (faster) or "tts-1-hd" (higher quality) */
|
|
26
|
+
model?: "tts-1" | "tts-1-hd";
|
|
27
|
+
/** Speech speed: 0.25 to 4.0, default 1.0 */
|
|
28
|
+
speed?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve TTS configuration from environment variables.
|
|
32
|
+
* Reads from AI_API_KEY, OPENAI_API_KEY, and AI_BASE_URL.
|
|
33
|
+
* @returns TTS configuration object
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveTTSConfig(): TTSConfig;
|
|
36
|
+
/**
|
|
37
|
+
* Check if TTS API key is configured.
|
|
38
|
+
* @returns True if API key is available
|
|
39
|
+
*/
|
|
40
|
+
export declare function hasTTSApiKey(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Convert text to speech using OpenAI TTS API.
|
|
43
|
+
*
|
|
44
|
+
* @param text - Text to convert to speech (must not be empty)
|
|
45
|
+
* @param options - TTS options including voice, model, and speed
|
|
46
|
+
* @returns Audio data as Uint8Array (MP3 format)
|
|
47
|
+
* @throws Error if text is empty, API key is missing, or API request fails
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const audio = await textToSpeech("你好,世界", { voice: "alloy" });
|
|
52
|
+
* // audio is a Uint8Array containing MP3 data
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function textToSpeech(text: string, options: TTSOptions): Promise<Uint8Array>;
|
|
56
|
+
/**
|
|
57
|
+
* Estimate the cost of TTS generation.
|
|
58
|
+
* OpenAI pricing: $15 per 1M characters for tts-1, $30 per 1M for tts-1-hd
|
|
59
|
+
*
|
|
60
|
+
* @param text - Text to estimate cost for
|
|
61
|
+
* @param model - TTS model to use
|
|
62
|
+
* @returns Estimated cost in USD
|
|
63
|
+
*/
|
|
64
|
+
export declare function estimateTTSCost(text: string, model?: "tts-1" | "tts-1-hd"): number;
|
|
65
|
+
//# sourceMappingURL=tts-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts-provider.d.ts","sourceRoot":"","sources":["../../../src/tools/lib/tts-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,CAAC;IACnB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iCAAiC;IACjC,KAAK,EAAE,WAAW,CAAC;IACnB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC7B,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,SAAS,CAK5C;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAoBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,UAAU,CAAC,CAwDrB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,OAAO,GAAG,UAAoB,GACpC,MAAM,CAIR"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS (Text-to-Speech) provider module for podcast generation.
|
|
3
|
+
* Supports OpenAI TTS API with proxy configuration.
|
|
4
|
+
*
|
|
5
|
+
* @module podcast/tts-provider
|
|
6
|
+
*/
|
|
7
|
+
import { fetch, ProxyAgent } from "undici";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve TTS configuration from environment variables.
|
|
10
|
+
* Reads from AI_API_KEY, OPENAI_API_KEY, and AI_BASE_URL.
|
|
11
|
+
* @returns TTS configuration object
|
|
12
|
+
*/
|
|
13
|
+
export function resolveTTSConfig() {
|
|
14
|
+
const apiKey = process.env.AI_API_KEY || process.env.OPENAI_API_KEY || "";
|
|
15
|
+
const baseUrl = process.env.AI_BASE_URL || "https://api.openai.com";
|
|
16
|
+
return { provider: "openai", apiKey, baseUrl };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if TTS API key is configured.
|
|
20
|
+
* @returns True if API key is available
|
|
21
|
+
*/
|
|
22
|
+
export function hasTTSApiKey() {
|
|
23
|
+
return resolveTTSConfig().apiKey.length > 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get proxy dispatcher if HTTPS_PROXY or HTTP_PROXY is set.
|
|
27
|
+
* @returns ProxyAgent if proxy is configured, undefined otherwise
|
|
28
|
+
*/
|
|
29
|
+
function getProxyDispatcher() {
|
|
30
|
+
const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
|
|
31
|
+
return proxyUrl ? new ProxyAgent(proxyUrl) : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Normalize base URL by removing trailing /v1 if present.
|
|
35
|
+
* @param baseUrl - Base URL to normalize
|
|
36
|
+
* @returns Normalized URL without trailing /v1
|
|
37
|
+
*/
|
|
38
|
+
function normalizeBaseUrl(baseUrl) {
|
|
39
|
+
return baseUrl.replace(/\/v1\/?$/, "");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert text to speech using OpenAI TTS API.
|
|
43
|
+
*
|
|
44
|
+
* @param text - Text to convert to speech (must not be empty)
|
|
45
|
+
* @param options - TTS options including voice, model, and speed
|
|
46
|
+
* @returns Audio data as Uint8Array (MP3 format)
|
|
47
|
+
* @throws Error if text is empty, API key is missing, or API request fails
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const audio = await textToSpeech("你好,世界", { voice: "alloy" });
|
|
52
|
+
* // audio is a Uint8Array containing MP3 data
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export async function textToSpeech(text, options) {
|
|
56
|
+
// Validate input
|
|
57
|
+
if (!text.trim()) {
|
|
58
|
+
throw new Error("Text cannot be empty");
|
|
59
|
+
}
|
|
60
|
+
const config = resolveTTSConfig();
|
|
61
|
+
if (!config.apiKey) {
|
|
62
|
+
throw new Error("TTS API Key not configured. Set AI_API_KEY or OPENAI_API_KEY environment variable.");
|
|
63
|
+
}
|
|
64
|
+
const model = options.model || "tts-1";
|
|
65
|
+
const speed = options.speed || 1.0;
|
|
66
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
67
|
+
// Build request
|
|
68
|
+
const dispatcher = getProxyDispatcher();
|
|
69
|
+
const requestBody = {
|
|
70
|
+
model,
|
|
71
|
+
input: text,
|
|
72
|
+
voice: options.voice,
|
|
73
|
+
speed,
|
|
74
|
+
response_format: "mp3",
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${baseUrl}/v1/audio/speech`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: {
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(requestBody),
|
|
84
|
+
...(dispatcher && { dispatcher }),
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const errText = await response.text();
|
|
88
|
+
const errMsg = errText.slice(0, 500);
|
|
89
|
+
throw new Error(`TTS API error (${response.status}): ${errMsg}`);
|
|
90
|
+
}
|
|
91
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
92
|
+
return new Uint8Array(arrayBuffer);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Re-throw if already our error
|
|
96
|
+
if (error instanceof Error && error.message.startsWith("TTS API error")) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
// Wrap other errors
|
|
100
|
+
throw new Error(`TTS request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Estimate the cost of TTS generation.
|
|
105
|
+
* OpenAI pricing: $15 per 1M characters for tts-1, $30 per 1M for tts-1-hd
|
|
106
|
+
*
|
|
107
|
+
* @param text - Text to estimate cost for
|
|
108
|
+
* @param model - TTS model to use
|
|
109
|
+
* @returns Estimated cost in USD
|
|
110
|
+
*/
|
|
111
|
+
export function estimateTTSCost(text, model = "tts-1") {
|
|
112
|
+
const charCount = text.length;
|
|
113
|
+
const pricePerMillion = model === "tts-1" ? 15 : 30;
|
|
114
|
+
return (charCount / 1_000_000) * pricePerMillion;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=tts-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tts-provider.js","sourceRoot":"","sources":["../../../src/tools/lib/tts-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types and interfaces for the podcast generation system.
|
|
3
|
+
*
|
|
4
|
+
* @module podcast/types
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Represents a voice/speaker in the podcast.
|
|
8
|
+
*/
|
|
9
|
+
export interface PodcastSpeaker {
|
|
10
|
+
/** Unique identifier: "host" or "guest" */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Display name (e.g., "主持人", "Host") */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Role in the conversation */
|
|
15
|
+
role: "host" | "guest";
|
|
16
|
+
/** OpenAI TTS voice ID */
|
|
17
|
+
voiceId: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A single line of dialogue in the podcast script.
|
|
21
|
+
*/
|
|
22
|
+
export interface DialogueSegment {
|
|
23
|
+
/** Speaker identifier: "host" or "guest" */
|
|
24
|
+
speaker: string;
|
|
25
|
+
/** The spoken text content */
|
|
26
|
+
text: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Complete podcast script for one blog article.
|
|
30
|
+
*/
|
|
31
|
+
export interface PodcastScript {
|
|
32
|
+
/** Article slug (e.g., "zh/getting-started") */
|
|
33
|
+
slug: string;
|
|
34
|
+
/** Article title */
|
|
35
|
+
title: string;
|
|
36
|
+
/** Language of the content */
|
|
37
|
+
lang: "zh" | "en";
|
|
38
|
+
/** Speakers used in this podcast */
|
|
39
|
+
speakers: PodcastSpeaker[];
|
|
40
|
+
/** Dialogue segments */
|
|
41
|
+
segments: DialogueSegment[];
|
|
42
|
+
/** ISO timestamp when generated */
|
|
43
|
+
generatedAt: string;
|
|
44
|
+
/** MD5 hash of source content */
|
|
45
|
+
contentHash: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Metadata for generated podcast audio file.
|
|
49
|
+
*/
|
|
50
|
+
export interface PodcastAudioMeta {
|
|
51
|
+
/** Article slug */
|
|
52
|
+
slug: string;
|
|
53
|
+
/** Language */
|
|
54
|
+
lang: "zh" | "en";
|
|
55
|
+
/** Duration in seconds */
|
|
56
|
+
duration: number;
|
|
57
|
+
/** File size in bytes */
|
|
58
|
+
fileSize: number;
|
|
59
|
+
/** Audio format */
|
|
60
|
+
format: "mp3";
|
|
61
|
+
/** ISO timestamp when generated */
|
|
62
|
+
generatedAt: string;
|
|
63
|
+
/** MD5 hash of source content */
|
|
64
|
+
contentHash: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Optional frontmatter configuration for podcast generation.
|
|
68
|
+
* Can be added to blog post frontmatter to customize podcast behavior.
|
|
69
|
+
*/
|
|
70
|
+
export interface PodcastFrontmatter {
|
|
71
|
+
/** Enable/disable podcast generation for this article */
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
/** Override default voice assignments */
|
|
74
|
+
voices?: {
|
|
75
|
+
host?: string;
|
|
76
|
+
guest?: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Cache structure for podcast scripts.
|
|
81
|
+
*/
|
|
82
|
+
export interface PodcastCache {
|
|
83
|
+
meta: {
|
|
84
|
+
lastUpdated: string | null;
|
|
85
|
+
totalProcessed: number;
|
|
86
|
+
};
|
|
87
|
+
articles: Record<string, PodcastScript>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Audio metadata cache structure.
|
|
91
|
+
*/
|
|
92
|
+
export interface PodcastAudioCache {
|
|
93
|
+
meta: {
|
|
94
|
+
lastUpdated: string | null;
|
|
95
|
+
totalProcessed: number;
|
|
96
|
+
};
|
|
97
|
+
articles: Record<string, PodcastAudioMeta>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Default speaker configurations for each language.
|
|
101
|
+
* Host uses "alloy" (neutral voice), Guest uses "onyx" (deeper voice).
|
|
102
|
+
*/
|
|
103
|
+
export declare const DEFAULT_SPEAKERS: Record<"zh" | "en", PodcastSpeaker[]>;
|
|
104
|
+
/**
|
|
105
|
+
* Available OpenAI TTS voices.
|
|
106
|
+
* @see https://platform.openai.com/docs/guides/text-to-speech/voice-options
|
|
107
|
+
*/
|
|
108
|
+
export declare const OPENAI_VOICES: readonly ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
|
|
109
|
+
/** OpenAI voice type */
|
|
110
|
+
export type OpenAIVoice = (typeof OPENAI_VOICES)[number];
|
|
111
|
+
/**
|
|
112
|
+
* Type guard to check if a string is a valid OpenAI voice.
|
|
113
|
+
* @param voice - Voice string to check
|
|
114
|
+
* @returns True if the voice is valid
|
|
115
|
+
*/
|
|
116
|
+
export declare function isValidOpenAIVoice(voice: string): voice is OpenAIVoice;
|
|
117
|
+
/**
|
|
118
|
+
* TTS models available from OpenAI.
|
|
119
|
+
*/
|
|
120
|
+
export declare const OPENAI_TTS_MODELS: readonly ["tts-1", "tts-1-hd"];
|
|
121
|
+
/** OpenAI TTS model type */
|
|
122
|
+
export type OpenAITTSModel = (typeof OPENAI_TTS_MODELS)[number];
|
|
123
|
+
/**
|
|
124
|
+
* Parse podcast frontmatter from a record (parsed YAML frontmatter).
|
|
125
|
+
* @param frontmatter - Parsed frontmatter object
|
|
126
|
+
* @returns PodcastFrontmatter or undefined
|
|
127
|
+
*/
|
|
128
|
+
export declare function parsePodcastFrontmatter(frontmatter: Record<string, unknown>): PodcastFrontmatter | undefined;
|
|
129
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/tools/lib/types.ts"],"names":[],"mappings":"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"}
|