Files
VoiceRSSSummary/services/llm.ts
Satsuki Akiba 6049a05776
Some checks failed
CI / security-scan (push) Has been skipped
Build and Publish Docker Images / build (push) Failing after 58s
CI / lint-and-test (push) Failing after 10m46s
CI / docker-test (push) Has been cancelled
Fix
2025-06-13 13:04:20 +09:00

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"}`,
);
}
}