Files
Harvey Zhao 740ac4cb24 Refactor styles in ArtPlayer components for improved readability and consistency
- Consolidated loading styles in loading.less for better structure.
- Updated mask styles in mask.less to enhance clarity.
- Refined mini popup styles in mini.less, adding transition effects.
- Improved mixins in mixins.less for consistent styling across components.
- Adjusted mobile styles in mobile.less for better responsiveness.
- Enhanced notice styles in notice.less for improved visibility.
- Streamlined plugin styles in plugins.less for better organization.
- Simplified poster styles in poster.less for clarity.
- Refined progress styles in progress.less, enhancing hover effects.
- Updated selector styles in selector.less for better interaction.
- Improved settings styles in setting.less for better usability.
- Enhanced subtitle styles in subtitle.less for better visibility.
- Updated variable definitions in variables.less for consistency.
- Refined video styles in video.less for improved layout.
- Enhanced volume control styles in volume.less for better user experience.
- Optimized rate limiting logic in build-llm.js and trans-docs.js for better performance.
2026-03-08 15:03:43 +08:00

263 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
import dotenv from 'dotenv'
import { glob } from 'glob'
dotenv.config()
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(__dirname, '../packages/artplayer-vitepress')
const srcDirs = ['docs/advanced', 'docs/component', 'docs/start']
const indexFile = 'docs/index.md'
const outputRoot = path.join(rootDir, 'docs/en')
const API_URL = 'https://api.deepseek.com/v1/chat/completions'
const API_KEY = process.env.DEEPSEEK_API_KEY
// 配置常量
const MAX_CONCURRENT_REQUESTS = 5 // 并发请求数
const REQUEST_TIMEOUT = 60000 // 请求超时时间 60秒
const MAX_RETRIES = 3 // 最大重试次数
const CHUNK_SIZE = 4000 // 分块大小
if (!API_KEY) {
console.error('❌ Missing DEEPSEEK_API_KEY in .env file')
process.exit(1)
}
// ==================== Markdown 修复功能 ====================
/**
* 修复 Markdown 代码块闭合问题
* - 遇到 ## 标题时补上遗漏的 ```
* - 遇到 ::: 时补上遗漏的 ```
* - 文件末尾检查是否需要补上
*/
function fixMarkdownCodeBlocks(content) {
const lines = content.split('\n')
const fixedLines = []
let inCodeBlock = false
let fixes = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// 检查是否进入代码块
if (line.startsWith('```') && !inCodeBlock) {
inCodeBlock = true
fixedLines.push(line)
continue
}
// 检查是否正常退出代码块
if (line.startsWith('```') && inCodeBlock) {
inCodeBlock = false
fixedLines.push(line)
continue
}
// 在代码块中遇到标题(缺少 ```)
if (inCodeBlock && line.startsWith('## ')) {
fixedLines.push('```')
fixedLines.push('')
inCodeBlock = false
fixes++
}
// 在代码块中遇到 :::(admonition)
if (inCodeBlock && line.startsWith(':::')) {
fixedLines.push('```')
fixedLines.push('')
inCodeBlock = false
fixes++
}
fixedLines.push(line)
}
// 文件末尾仍在代码块中
if (inCodeBlock) {
fixedLines.push('```')
fixes++
}
return { content: fixedLines.join('\n'), fixes }
}
// ==================== 翻译功能 ====================
function splitMarkdown(text, maxLen = CHUNK_SIZE) {
const lines = text.split('\n')
const chunks = []
let buffer = ''
let insideCodeBlock = false
for (const line of lines) {
if (line.trim().startsWith('```'))
insideCodeBlock = !insideCodeBlock
if ((`${buffer}\n${line}`).length > maxLen && !insideCodeBlock) {
chunks.push(buffer)
buffer = ''
}
buffer += `\n${line}`
}
if (buffer.trim())
chunks.push(buffer)
return chunks
}
function cleanTranslation(text) {
return text
.replace(/^```(markdown|md)?/gi, '')
.replace(/```$/g, '')
.replace(/^(Here(')s|Below is|Translation|The English version|Here is)[::]?\s*/gi, '')
.replace(/(Translation completed\.?|End of translation\.?)$/gi, '')
.trim()
}
async function callDeepSeekAPI(prompt, tag, chunkId) {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
console.log(`🔹 [${tag}] Translating chunk ${chunkId} (attempt ${attempt}/${MAX_RETRIES})`)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
const res = await fetch(API_URL, {
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: 'deepseek-chat',
messages: [{ role: 'user', content: prompt }],
temperature: 0.2,
}),
})
clearTimeout(timeout)
// 处理限流情况
if (res.status === 429) {
const delay = 2 ** attempt * 1000
console.log(`⏳ [${tag}] Rate limited, waiting ${delay}ms...`)
await new Promise(r => setTimeout(r, delay))
continue
}
if (!res.ok) {
const error = await res.text()
console.error(`[${tag}] API Error:`, error)
throw new Error(`DeepSeek API error: ${res.status}`)
}
const data = await res.json()
if (!data.choices || !data.choices[0]) {
throw new Error('Invalid API response: missing choices')
}
return data.choices[0].message?.content?.trim() || ''
}
catch (err) {
if (err.name === 'AbortError') {
console.error(`[${tag}] Request timeout for chunk ${chunkId}`)
}
if (attempt === MAX_RETRIES) {
throw err
}
const delay = 1000 * attempt
console.log(`⚠️ [${tag}] Retry ${attempt}/${MAX_RETRIES} after ${delay}ms...`)
await new Promise(r => setTimeout(r, delay))
}
}
}
async function translateText(text, fileName) {
const chunks = splitMarkdown(text, CHUNK_SIZE)
let result = ''
for (let i = 0; i < chunks.length; i++) {
const prompt = `
You are a professional English technical translator for documentation.
Translate the following Markdown content from Simplified Chinese to fluent English.
Keep all Markdown structure, code blocks, and formatting intact.
Return ONLY the translated Markdown content — do NOT add any explanations, prefixes, or summaries.
Text to translate:
${chunks[i]}
`.trim()
const raw = await callDeepSeekAPI(prompt, fileName, `${i + 1}/${chunks.length}`)
const translated = cleanTranslation(raw)
result += `\n\n${translated}`
}
return result.trim()
}
async function processFile(inputPath, outputPath) {
const content = fs.readFileSync(inputPath, 'utf8')
const fileName = path.basename(inputPath)
console.log(`🌍 Translating: ${inputPath}`)
// 1. 翻译
let translated = await translateText(content, fileName)
// 2. 修复 Markdown 语法
const { content: fixed, fixes } = fixMarkdownCodeBlocks(translated)
if (fixes > 0) {
console.log(`🔧 [${fileName}] Fixed ${fixes} unclosed code blocks`)
translated = fixed
}
// 3. 保存
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
fs.writeFileSync(outputPath, translated, 'utf8')
console.log(`✅ Saved: ${outputPath}`)
}
async function runWithConcurrency(tasks, limit = MAX_CONCURRENT_REQUESTS) {
const results = []
const executing = new Set()
for (const task of tasks) {
const p = task().finally(() => executing.delete(p))
results.push(p)
executing.add(p)
if (executing.size >= limit) {
await Promise.race(executing)
}
}
return Promise.all(results)
}
async function main() {
console.log('🚀 Starting translation...')
fs.rmSync(outputRoot, { recursive: true, force: true })
fs.mkdirSync(outputRoot, { recursive: true })
const indexPath = path.join(rootDir, indexFile)
const indexOut = path.join(outputRoot, 'index.md')
await processFile(indexPath, indexOut)
const tasks = []
for (const dir of srcDirs) {
const fullDir = path.join(rootDir, dir)
const files = await glob(`${fullDir}/**/*.md`)
for (const file of files) {
const relative = path.relative(rootDir, file)
const outputFile = path.join(outputRoot, relative.replace(/^docs[\\/]/, ''))
tasks.push(() => processFile(file, outputFile))
}
}
console.log(`🧠 Total files: ${tasks.length}, concurrency: ${MAX_CONCURRENT_REQUESTS}`)
await runWithConcurrency(tasks, MAX_CONCURRENT_REQUESTS)
console.log('🎉 Translation complete!')
}
main().catch((err) => {
console.error('❌ Translation failed:', err)
process.exit(1)
})