- 新規記事検出システム: 記事の重複チェックと新規記事のみ処理 - 記事単位ポッドキャスト: フィード統合から記事個別エピソードに変更 - 6時間間隔バッチ処理: 自動定期実行スケジュールの改善 - 完全UIリニューアル: ダッシュボード・フィード管理・エピソード管理の3画面構成 - アクセシビリティ強化: ARIA属性、キーボードナビ、高コントラスト対応 - データベース刷新: feeds/articles/episodes階層構造への移行 - 中央集権設定管理: services/config.ts による設定統一 - エラーハンドリング改善: 全モジュールでの堅牢なエラー処理 - TypeScript型安全性向上: null安全性とインターフェース改善 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
117 lines
3.1 KiB
TypeScript
117 lines
3.1 KiB
TypeScript
import path from "path";
|
|
|
|
interface Config {
|
|
// OpenAI Configuration
|
|
openai: {
|
|
apiKey: string;
|
|
endpoint: string;
|
|
modelName: string;
|
|
};
|
|
|
|
// VOICEVOX Configuration
|
|
voicevox: {
|
|
host: string;
|
|
styleId: number;
|
|
};
|
|
|
|
// Podcast Configuration
|
|
podcast: {
|
|
title: string;
|
|
link: string;
|
|
description: string;
|
|
language: string;
|
|
author: string;
|
|
categories: string;
|
|
ttl: string;
|
|
baseUrl: string;
|
|
};
|
|
|
|
// File paths
|
|
paths: {
|
|
projectRoot: string;
|
|
dataDir: string;
|
|
dbPath: string;
|
|
publicDir: string;
|
|
podcastAudioDir: string;
|
|
frontendBuildDir: string;
|
|
feedUrlsFile: string;
|
|
};
|
|
}
|
|
|
|
function getRequiredEnv(key: string): string {
|
|
const value = import.meta.env[key];
|
|
if (!value) {
|
|
throw new Error(`Required environment variable ${key} is not set`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function getOptionalEnv(key: string, defaultValue: string): string {
|
|
return import.meta.env[key] ?? defaultValue;
|
|
}
|
|
|
|
function createConfig(): Config {
|
|
const projectRoot = import.meta.dirname ? path.dirname(import.meta.dirname) : process.cwd();
|
|
const dataDir = path.join(projectRoot, "data");
|
|
const publicDir = path.join(projectRoot, "public");
|
|
|
|
return {
|
|
openai: {
|
|
apiKey: getRequiredEnv("OPENAI_API_KEY"),
|
|
endpoint: getOptionalEnv("OPENAI_API_ENDPOINT", "https://api.openai.com/v1"),
|
|
modelName: getOptionalEnv("OPENAI_MODEL_NAME", "gpt-4o-mini"),
|
|
},
|
|
|
|
voicevox: {
|
|
host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"),
|
|
styleId: parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
|
|
},
|
|
|
|
podcast: {
|
|
title: getOptionalEnv("PODCAST_TITLE", "自動生成ポッドキャスト"),
|
|
link: getOptionalEnv("PODCAST_LINK", "https://your-domain.com/podcast"),
|
|
description: getOptionalEnv("PODCAST_DESCRIPTION", "RSSフィードから自動生成された音声ポッドキャスト"),
|
|
language: getOptionalEnv("PODCAST_LANGUAGE", "ja"),
|
|
author: getOptionalEnv("PODCAST_AUTHOR", "管理者"),
|
|
categories: getOptionalEnv("PODCAST_CATEGORIES", "Technology"),
|
|
ttl: getOptionalEnv("PODCAST_TTL", "60"),
|
|
baseUrl: getOptionalEnv("PODCAST_BASE_URL", "https://your-domain.com"),
|
|
},
|
|
|
|
paths: {
|
|
projectRoot,
|
|
dataDir,
|
|
dbPath: path.join(dataDir, "podcast.db"),
|
|
publicDir,
|
|
podcastAudioDir: path.join(publicDir, "podcast_audio"),
|
|
frontendBuildDir: path.join(projectRoot, "frontend", "dist"),
|
|
feedUrlsFile: path.join(projectRoot, getOptionalEnv("FEED_URLS_FILE", "feed_urls.txt")),
|
|
},
|
|
};
|
|
}
|
|
|
|
export const config = createConfig();
|
|
|
|
export function validateConfig(): void {
|
|
// Validate required configuration
|
|
if (!config.openai.apiKey) {
|
|
throw new Error("OPENAI_API_KEY is required");
|
|
}
|
|
|
|
if (isNaN(config.voicevox.styleId)) {
|
|
throw new Error("VOICEVOX_STYLE_ID must be a valid number");
|
|
}
|
|
|
|
// Validate URLs
|
|
try {
|
|
new URL(config.voicevox.host);
|
|
} catch {
|
|
throw new Error("VOICEVOX_HOST must be a valid URL");
|
|
}
|
|
|
|
try {
|
|
new URL(config.openai.endpoint);
|
|
} catch {
|
|
throw new Error("OPENAI_API_ENDPOINT must be a valid URL");
|
|
}
|
|
} |