Skip to content

Commit 047fb32

Browse files
author
y-yamasaki
committed
自動翻訳調整
1 parent 2badf08 commit 047fb32

1 file changed

Lines changed: 131 additions & 43 deletions

File tree

WebSite/tools/auto-translate.js

Lines changed: 131 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
/**
22
* @file auto-translate.js
3-
* @description Gemini API を使用して日本語ブログ記事を英語に自動翻訳するスクリプト
3+
* @description Gemini API または GitHub Models を使用して日本語ブログ記事を英語に自動翻訳するスクリプト
44
* @summary
55
* - content/blog 内の .md ファイルを読み込み、英語版 (.en.md) を生成
66
* - フロントマター(title, description, category, tags)と本文を翻訳
77
* - 既に英語版が存在する場合はスキップ
8-
* @recent_changes
9-
* - ANSIカラーコード出力を削除し、可読性を向上
10-
* - 簡易ロガー関数を追加(本番環境では verbose ログを抑制)
8+
* - 環境変数 TRANSLATION_PROVIDER で 'gemini' または 'github' を切り替え可能
119
*/
1210

1311
const fs = require("fs");
@@ -20,29 +18,72 @@ require("dotenv").config();
2018
// ───────────────────────────────────────────────────────────────
2119
const isProduction = process.env.NODE_ENV === "production";
2220
const logger = {
23-
info: (msg) => !isProduction && console.log(`[INFO] ${msg}`),
21+
info: (msg) =>
22+
!isProduction && console.log(`[INFO] ${msg}`),
2423
warn: (msg) => console.warn(`[WARN] ${msg}`),
2524
error: (msg) => console.error(`[ERROR] ${msg}`),
2625
success: (msg) => console.log(`[SUCCESS] ${msg}`),
2726
};
2827

29-
// Configuration
30-
const API_KEY = process.env.GEMINI_API_KEY;
31-
const MODEL =
32-
process.env.GEMINI_MODEL || "gemini-2.0-flash"; // 2.5-proは未リリースの可能性があるため、動作確認済みのモデルまたは環境変数に従う
33-
const API_ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${API_KEY}`;
28+
// ───────────────────────────────────────────────────────────────
29+
// Configuration & Validation
30+
// ───────────────────────────────────────────────────────────────
3431

35-
const ROOT_DIR = path.join(__dirname, "..");
36-
const BLOG_DIR = path.join(ROOT_DIR, "content", "blog");
32+
// プロバイダーの決定: 環境変数指定 -> GeminiキーがあるならGemini -> GitHubキーがあるならGitHub
33+
let provider = process.env.TRANSLATION_PROVIDER;
34+
if (!provider) {
35+
if (process.env.GEMINI_API_KEY) {
36+
provider = "gemini";
37+
} else if (process.env.GITHUB_TOKEN) {
38+
provider = "github";
39+
}
40+
}
41+
42+
// Gemini Config
43+
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
44+
const GEMINI_MODEL =
45+
process.env.GEMINI_MODEL || "gemini-2.0-flash";
46+
const GEMINI_ENDPOINT = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;
47+
48+
// GitHub Models Config
49+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
50+
const GITHUB_MODEL = process.env.GITHUB_MODEL || "gpt-4o";
51+
const GITHUB_ENDPOINT =
52+
"https://models.inference.ai.azure.com/chat/completions";
3753

38-
// Validate API Key
39-
if (!API_KEY) {
40-
logger.error("GEMINI_API_KEY is not set.");
41-
logger.error("Please set the GEMINI_API_KEY environment variable.");
42-
logger.error("Example: export GEMINI_API_KEY=AIza...");
54+
// Validate Configuration
55+
if (provider === "gemini") {
56+
if (!GEMINI_API_KEY) {
57+
logger.error("GEMINI_API_KEY is not set.");
58+
process.exit(1);
59+
}
60+
logger.info(`Using Provider: Gemini (${GEMINI_MODEL})`);
61+
} else if (provider === "github") {
62+
if (!GITHUB_TOKEN) {
63+
logger.error("GITHUB_TOKEN is not set.");
64+
logger.error(
65+
"Please set GITHUB_TOKEN to use GitHub Models.",
66+
);
67+
process.exit(1);
68+
}
69+
logger.info(
70+
`Using Provider: GitHub Models (${GITHUB_MODEL})`,
71+
);
72+
} else {
73+
logger.error("No valid translation provider found.");
74+
logger.error(
75+
"Please set GEMINI_API_KEY or GITHUB_TOKEN in your .env file.",
76+
);
4377
process.exit(1);
4478
}
4579

80+
const ROOT_DIR = path.join(__dirname, "..");
81+
const BLOG_DIR = path.join(ROOT_DIR, "content", "blog");
82+
83+
// ───────────────────────────────────────────────────────────────
84+
// Translation Logic
85+
// ───────────────────────────────────────────────────────────────
86+
4687
async function translateText(text, context = "") {
4788
if (!text) return "";
4889

@@ -54,6 +95,14 @@ Do not translate the file path in the image link.
5495
${context ? `Context: ${context}` : ""}
5596
`;
5697

98+
if (provider === "gemini") {
99+
return await translateWithGemini(text, systemPrompt);
100+
} else {
101+
return await translateWithGitHub(text, systemPrompt);
102+
}
103+
}
104+
105+
async function translateWithGemini(text, systemPrompt) {
57106
const requestBody = {
58107
system_instruction: {
59108
parts: { text: systemPrompt },
@@ -64,7 +113,6 @@ ${context ? `Context: ${context}` : ""}
64113
parts: [{ text: text }],
65114
},
66115
],
67-
// セーフティ設定を追加し、誤検知による停止を防ぐ
68116
safetySettings: [
69117
{
70118
category: "HARM_CATEGORY_HARASSMENT",
@@ -85,79 +133,118 @@ ${context ? `Context: ${context}` : ""}
85133
],
86134
generationConfig: {
87135
temperature: 0.3,
88-
maxOutputTokens: 8192, // 長文での途中切れを防ぐため十分なトークン数を確保
136+
maxOutputTokens: 8192,
89137
},
90138
};
91139

92140
try {
93-
const response = await fetch(API_ENDPOINT, {
141+
const response = await fetch(GEMINI_ENDPOINT, {
94142
method: "POST",
95-
headers: {
96-
"Content-Type": "application/json",
97-
},
143+
headers: { "Content-Type": "application/json" },
98144
body: JSON.stringify(requestBody),
99145
});
100146

101147
if (!response.ok) {
102148
const error = await response.json();
103149
throw new Error(
104-
`API Error: ${response.status} - ${JSON.stringify(error)}`,
150+
`Gemini API Error: ${response.status} - ${JSON.stringify(error)}`,
105151
);
106152
}
107153

108154
const data = await response.json();
109155

110156
if (!data.candidates || !data.candidates[0]) {
111-
logger.warn(`Unexpected response format: ${JSON.stringify(data)}`);
112157
throw new Error(
113158
"Failed to parse Gemini response: No candidates found",
114159
);
115160
}
116161

117162
const candidate = data.candidates[0];
118-
119-
// 終了理由を確認し、正常終了でない場合は警告を出す
120163
if (
121164
candidate.finishReason &&
122165
candidate.finishReason !== "STOP"
123166
) {
124-
logger.warn(`Translation stopped early. Reason: ${candidate.finishReason}`);
125-
// SAFETY等の理由でコンテンツが空の場合はエラーとする
126-
if (
127-
!candidate.content ||
128-
!candidate.content.parts ||
129-
!candidate.content.parts[0].text
130-
) {
167+
logger.warn(
168+
`Translation stopped early. Reason: ${candidate.finishReason}`,
169+
);
170+
if (!candidate.content?.parts?.[0]?.text) {
131171
throw new Error(
132172
`Generation blocked due to: ${candidate.finishReason}`,
133173
);
134174
}
135175
}
136176

177+
return candidate.content.parts[0].text.trim();
178+
} catch (error) {
179+
logger.error(
180+
`Gemini Translation failed: ${error.message}`,
181+
);
182+
throw error;
183+
}
184+
}
185+
186+
async function translateWithGitHub(text, systemPrompt) {
187+
const requestBody = {
188+
model: GITHUB_MODEL,
189+
messages: [
190+
{ role: "system", content: systemPrompt },
191+
{ role: "user", content: text },
192+
],
193+
temperature: 0.3,
194+
max_tokens: 4096,
195+
};
196+
197+
try {
198+
const response = await fetch(GITHUB_ENDPOINT, {
199+
method: "POST",
200+
headers: {
201+
"Content-Type": "application/json",
202+
Authorization: `Bearer ${GITHUB_TOKEN}`,
203+
},
204+
body: JSON.stringify(requestBody),
205+
});
206+
207+
if (!response.ok) {
208+
const error = await response.json();
209+
throw new Error(
210+
`GitHub API Error: ${response.status} - ${JSON.stringify(error)}`,
211+
);
212+
}
213+
214+
const data = await response.json();
215+
137216
if (
138-
!candidate.content ||
139-
!candidate.content.parts ||
140-
!candidate.content.parts[0].text
217+
!data.choices ||
218+
!data.choices[0] ||
219+
!data.choices[0].message
141220
) {
142221
throw new Error(
143-
"Failed to parse Gemini response: No text content",
222+
"Failed to parse GitHub response: Invalid format",
144223
);
145224
}
146225

147-
return candidate.content.parts[0].text.trim();
226+
return data.choices[0].message.content.trim();
148227
} catch (error) {
149-
logger.error(`Translation failed: ${error.message}`);
228+
logger.error(
229+
`GitHub Translation failed: ${error.message}`,
230+
);
150231
throw error;
151232
}
152233
}
153234

235+
// ───────────────────────────────────────────────────────────────
236+
// File Processing
237+
// ───────────────────────────────────────────────────────────────
238+
154239
async function processFile(filePath) {
155240
const fileName = path.basename(filePath);
156241
const id = path.basename(filePath, ".md");
157242
const enFilePath = path.join(BLOG_DIR, `${id}.en.md`);
158243

159244
if (fs.existsSync(enFilePath)) {
160-
logger.info(`Skipping ${fileName}: English version already exists.`);
245+
logger.info(
246+
`Skipping ${fileName}: English version already exists.`,
247+
);
161248
return;
162249
}
163250

@@ -194,7 +281,6 @@ async function processFile(filePath) {
194281
let translatedTags = [];
195282
if (Array.isArray(frontmatter.tags)) {
196283
for (const tag of frontmatter.tags) {
197-
// Skip translation for simple ASCII tags, translate others
198284
if (
199285
/[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf]/.test(
200286
tag,
@@ -233,7 +319,9 @@ async function processFile(filePath) {
233319
fs.writeFileSync(enFilePath, newContent, "utf8");
234320
logger.success(`Created ${id}.en.md`);
235321
} catch (error) {
236-
logger.error(`Failed to process ${fileName}: ${error.message}`);
322+
logger.error(
323+
`Failed to process ${fileName}: ${error.message}`,
324+
);
237325
}
238326
}
239327

0 commit comments

Comments
 (0)