241 lines
7.1 KiB
TypeScript
241 lines
7.1 KiB
TypeScript
import { type ClientOptions, OpenAI } from "openai";
|
|
import { config, validateConfig } from "./config.js";
|
|
|
|
// Validate config on module load
|
|
validateConfig();
|
|
|
|
const clientOptions: ClientOptions = {
|
|
apiKey: config.openai.apiKey,
|
|
baseURL: config.openai.endpoint,
|
|
};
|
|
const openai = new OpenAI(clientOptions);
|
|
|
|
export async function openAI_ClassifyFeed(title: string): Promise<string> {
|
|
if (!title || title.trim() === "") {
|
|
throw new Error("Feed title is required for classification");
|
|
}
|
|
|
|
const prompt = `
|
|
以下のRSSフィードのタイトルを見て、適切なトピックカテゴリに分類してください。
|
|
|
|
フィードタイトル: ${title}
|
|
|
|
以下のカテゴリから1つを選択してください:
|
|
- テクノロジー
|
|
- ビジネス
|
|
- エンターテインメント
|
|
- スポーツ
|
|
- 科学
|
|
- 健康
|
|
- 政治
|
|
- 環境
|
|
- 教育
|
|
- その他
|
|
|
|
分類結果を上記カテゴリのいずれか1つだけ返してください。
|
|
`;
|
|
|
|
try {
|
|
const response = await openai.chat.completions.create({
|
|
model: config.openai.modelName,
|
|
messages: [{ role: "user", content: prompt.trim() }],
|
|
temperature: 0.2,
|
|
reasoning_effort: "low",
|
|
});
|
|
|
|
const category = response.choices[0]?.message?.content?.trim();
|
|
if (!category) {
|
|
console.warn("OpenAI returned empty category, using default");
|
|
return "その他";
|
|
}
|
|
|
|
return category;
|
|
} catch (error) {
|
|
console.error("Error classifying feed:", error);
|
|
throw new Error(
|
|
`Failed to classify feed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function openAI_GeneratePodcastContent(
|
|
title: string,
|
|
items: Array<{
|
|
title: string;
|
|
link: string;
|
|
content?: string;
|
|
description?: string;
|
|
}>,
|
|
): Promise<string> {
|
|
if (!title || title.trim() === "") {
|
|
throw new Error("Feed title is required for podcast content generation");
|
|
}
|
|
|
|
if (!items || items.length === 0) {
|
|
throw new Error(
|
|
"At least one news item is required for podcast content generation",
|
|
);
|
|
}
|
|
|
|
// Validate items
|
|
const validItems = items.filter((item) => item.title && item.link);
|
|
if (validItems.length === 0) {
|
|
throw new Error("No valid news items found (title and link required)");
|
|
}
|
|
|
|
// Build detailed article information including content
|
|
const articleDetails = validItems
|
|
.map((item, i) => {
|
|
let articleInfo = `${i + 1}. タイトル: ${item.title}\nURL: ${item.link}`;
|
|
|
|
// Add content if available
|
|
const content = item.content || item.description;
|
|
if (content && content.trim()) {
|
|
// Limit content length to avoid token limits
|
|
const maxContentLength = 2000;
|
|
const truncatedContent =
|
|
content.length > maxContentLength
|
|
? content.substring(0, maxContentLength) + "..."
|
|
: content;
|
|
articleInfo += `\n内容: ${truncatedContent}`;
|
|
}
|
|
|
|
return articleInfo;
|
|
})
|
|
.join("\n\n");
|
|
|
|
const prompt = `
|
|
You are a professional podcaster. Create a detailed podcast script based on the provided feed title/topic, following these specific requirements:
|
|
|
|
**Content Requirements:**
|
|
- Provide detailed summaries and analysis based on the actual content of news articles
|
|
- Translate all news article titles to Japanese and introduce them at the beginning
|
|
- Create substantive commentary that adds value beyond just reading headlines
|
|
- Conclude with a comprehensive summary of the overall topic
|
|
- The podcast name must be "自動生成ポッドキャスト"
|
|
|
|
**Format Requirements:**
|
|
- Length: 1000-5000 characters (Japanese)
|
|
- Use natural, conversational Japanese language suitable for audio
|
|
- Write in a engaging podcast style with smooth transitions between topics
|
|
- There is only **one speaker** in the podcast, so use first-person perspective
|
|
|
|
**Technical Requirements:**
|
|
- Since this will be read by Japanese Text-to-Speech, convert ALL English words, abbreviations, and acronyms to katakana
|
|
- Examples:
|
|
- "AI" → "エーアイ"
|
|
- "NASA" → "ナサ"
|
|
- "GitHub" → "ギットハブ"
|
|
- "OpenAI" → "オープンエーアイ"
|
|
- Don't include any urls in the script.
|
|
- Don't include any non-Japanese characters in the script.
|
|
- Apply this conversion consistently throughout the script
|
|
|
|
**Structure:**
|
|
1. Opening with translated article titles
|
|
2. Detailed discussion of each news item with context and analysis
|
|
3. Overall topic summary and conclusion
|
|
|
|
Create a valuable, informative podcast script that utilizes the actual article content rather than just surface-level information.
|
|
`;
|
|
|
|
const sendContent = `
|
|
FeedTitle: ${title}
|
|
Content:
|
|
${articleDetails}
|
|
`;
|
|
|
|
try {
|
|
const response = await openai.chat.completions.create({
|
|
model: config.openai.modelName,
|
|
messages: [
|
|
{ role: "system", content: prompt.trim() },
|
|
{ role: "user", content: sendContent.trim() },
|
|
],
|
|
temperature: 0.4,
|
|
max_completion_tokens: 1700,
|
|
reasoning_effort: "medium",
|
|
});
|
|
|
|
const scriptText = response.choices[0]?.message?.content?.trim();
|
|
if (!scriptText) {
|
|
throw new Error("OpenAI returned empty podcast content");
|
|
}
|
|
|
|
return scriptText;
|
|
} catch (error) {
|
|
console.error("Error generating podcast content:", error);
|
|
throw new Error(
|
|
`Failed to generate podcast content: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function openAI_ClassifyEpisode(
|
|
title: string,
|
|
description?: string,
|
|
content?: string,
|
|
): Promise<string> {
|
|
if (!title || title.trim() === "") {
|
|
throw new Error("Episode title is required for classification");
|
|
}
|
|
|
|
// Build the text for classification based on available data
|
|
let textForClassification = `タイトル: ${title}`;
|
|
|
|
if (description && description.trim()) {
|
|
textForClassification += `\n説明: ${description}`;
|
|
}
|
|
|
|
if (content && content.trim()) {
|
|
const maxContentLength = 1500;
|
|
const truncatedContent =
|
|
content.length > maxContentLength
|
|
? content.substring(0, maxContentLength) + "..."
|
|
: content;
|
|
textForClassification += `\n内容: ${truncatedContent}`;
|
|
}
|
|
|
|
const prompt = `
|
|
以下のポッドキャストエピソードの情報を見て、適切なトピックカテゴリに分類してください。
|
|
|
|
${textForClassification}
|
|
|
|
以下のカテゴリから1つを選択してください:
|
|
- テクノロジー
|
|
- ビジネス
|
|
- エンターテインメント
|
|
- スポーツ
|
|
- 科学
|
|
- 健康
|
|
- 政治
|
|
- 環境
|
|
- 教育
|
|
- その他
|
|
|
|
エピソードの内容に最も適合するカテゴリを上記から1つだけ返してください。
|
|
`;
|
|
|
|
try {
|
|
const response = await openai.chat.completions.create({
|
|
model: config.openai.modelName,
|
|
messages: [{ role: "user", content: prompt.trim() }],
|
|
temperature: 0.2,
|
|
reasoning_effort: "low",
|
|
});
|
|
|
|
const category = response.choices[0]?.message?.content?.trim();
|
|
if (!category) {
|
|
console.warn("OpenAI returned empty episode category, using default");
|
|
return "その他";
|
|
}
|
|
|
|
return category;
|
|
} catch (error) {
|
|
console.error("Error classifying episode:", error);
|
|
throw new Error(
|
|
`Failed to classify episode: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
);
|
|
}
|
|
}
|