Files
bilibili-subtitle/src/hooks/useTranslate.ts
2024-09-19 15:08:15 +08:00

364 lines
12 KiB
TypeScript

import {useAppDispatch, useAppSelector} from './redux'
import {useCallback} from 'react'
import {
addTaskId,
addTransResults,
delTaskId,
mergeAskInfo,
setLastSummarizeTime,
setLastTransTime,
setSummaryContent,
setSummaryError,
setSummaryStatus,
setReviewAction,
setTempData
} from '../redux/envReducer'
import {
LANGUAGE_DEFAULT,
LANGUAGES_MAP,
PROMPT_DEFAULTS,
PROMPT_TYPE_ASK,
PROMPT_TYPE_TRANSLATE,
SUMMARIZE_LANGUAGE_DEFAULT,
SUMMARIZE_THRESHOLD,
SUMMARIZE_TYPES,
TRANSLATE_COOLDOWN,
TRANSLATE_FETCH_DEFAULT,
} from '../const'
import toast from 'react-hot-toast'
import {useMemoizedFn} from 'ahooks/es'
import {extractJsonArray, extractJsonObject, getModel} from '../util/biz_util'
import {formatTime} from '../util/util'
const useTranslate = () => {
const dispatch = useAppDispatch()
const data = useAppSelector(state => state.env.data)
const curIdx = useAppSelector(state => state.env.curIdx)
const lastTransTime = useAppSelector(state => state.env.lastTransTime)
const transResults = useAppSelector(state => state.env.transResults)
const envData = useAppSelector(state => state.env.envData)
const language = LANGUAGES_MAP[envData.language??LANGUAGE_DEFAULT]
const summarizeLanguage = LANGUAGES_MAP[envData.summarizeLanguage??SUMMARIZE_LANGUAGE_DEFAULT]
const title = useAppSelector(state => state.env.title)
const reviewed = useAppSelector(state => state.env.tempData.reviewed)
const reviewAction = useAppSelector(state => state.env.reviewAction)
const reviewActions = useAppSelector(state => state.env.tempData.reviewActions)
/**
* 获取下一个需要翻译的行
* 会检测冷却
*/
const getFetch = useCallback(() => {
if (data?.body != null && data.body.length > 0) {
const curIdx_ = curIdx ?? 0
// check lastTransTime
if (lastTransTime && Date.now() - lastTransTime < TRANSLATE_COOLDOWN) {
return
}
let nextIdleIdx
for (let i = curIdx_; i < data.body.length; i++) {
if (transResults[i] == null) {
nextIdleIdx = i
break
}
}
if (nextIdleIdx != null && nextIdleIdx - curIdx_ <= Math.ceil((envData.fetchAmount??TRANSLATE_FETCH_DEFAULT)/2)) {
return nextIdleIdx
}
}
}, [curIdx, data?.body, envData.fetchAmount, lastTransTime, transResults])
const addTask = useCallback(async (startIdx: number) => {
if ((data?.body) != null) {
const lines: string[] = data.body.slice(startIdx, startIdx + (envData.fetchAmount??TRANSLATE_FETCH_DEFAULT)).map((item: any) => item.content)
if (lines.length > 0) {
const linesMap: {[key: string]: string} = {}
lines.forEach((line, idx) => {
linesMap[(idx + 1)+''] = line
})
let lineStr = JSON.stringify(linesMap).replaceAll('\n', '')
lineStr = '```' + lineStr + '```'
let prompt: string = envData.prompts?.[PROMPT_TYPE_TRANSLATE]??PROMPT_DEFAULTS[PROMPT_TYPE_TRANSLATE]
// replace params
prompt = prompt.replaceAll('{{language}}', language.name)
prompt = prompt.replaceAll('{{title}}', title??'')
prompt = prompt.replaceAll('{{subtitles}}', lineStr)
const taskDef: TaskDef = {
type: envData.aiType === 'gemini'?'geminiChatComplete':'chatComplete',
serverUrl: envData.serverUrl,
data: envData.aiType === 'gemini'
?{
contents: [
{
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
maxOutputTokens: 2048
}
}
:{
model: getModel(envData),
messages: [
{
role: 'user',
content: prompt,
}
],
temperature: 0.25,
n: 1,
stream: false,
},
extra: {
type: 'translate',
apiKey: envData.apiKey,
geminiApiKey: envData.geminiApiKey,
startIdx,
size: lines.length,
}
}
console.debug('addTask', taskDef)
dispatch(setLastTransTime(Date.now()))
// addTransResults
const result: { [key: number]: TransResult } = {}
lines.forEach((line, idx) => {
result[startIdx + idx] = {
// idx: startIdx + idx,
}
})
dispatch(addTransResults(result))
const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef})
dispatch(addTaskId(task.id))
}
}
}, [data?.body, envData, language.name, title, dispatch])
const addSummarizeTask = useCallback(async (type: SummaryType, segment: Segment) => {
//review action
if (reviewed === undefined && !reviewAction) {
dispatch(setReviewAction(true))
dispatch(setTempData({
reviewActions: (reviewActions ?? 0) + 1
}))
}
if (segment.text.length >= SUMMARIZE_THRESHOLD) {
let subtitles = ''
for (const item of segment.items) {
subtitles += formatTime(item.from) + ' ' + item.content + '\n'
}
// @ts-expect-error
const promptType: keyof typeof PROMPT_DEFAULTS = SUMMARIZE_TYPES[type].promptType
let prompt: string = envData.prompts?.[promptType]??PROMPT_DEFAULTS[promptType]
// replace params
prompt = prompt.replaceAll('{{language}}', summarizeLanguage.name)
prompt = prompt.replaceAll('{{title}}', title??'')
prompt = prompt.replaceAll('{{subtitles}}', subtitles)
prompt = prompt.replaceAll('{{segment}}', segment.text)
const taskDef: TaskDef = {
type: envData.aiType === 'gemini'?'geminiChatComplete':'chatComplete',
serverUrl: envData.serverUrl,
data: envData.aiType === 'gemini'
?{
contents: [
{
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
maxOutputTokens: 2048
}
}
:{
model: getModel(envData),
messages: [
{
role: 'user',
content: prompt,
}
],
temperature: 0.5,
n: 1,
stream: false,
},
extra: {
type: 'summarize',
summaryType: type,
startIdx: segment.startIdx,
apiKey: envData.apiKey,
geminiApiKey: envData.geminiApiKey,
}
}
console.debug('addSummarizeTask', taskDef)
dispatch(setSummaryStatus({segmentStartIdx: segment.startIdx, type, status: 'pending'}))
dispatch(setLastSummarizeTime(Date.now()))
const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef})
dispatch(addTaskId(task.id))
}
}, [dispatch, envData, summarizeLanguage.name, title])
const addAskTask = useCallback(async (id: string, segment: Segment, question: string) => {
if (segment.text.length >= SUMMARIZE_THRESHOLD) {
let prompt: string = envData.prompts?.[PROMPT_TYPE_ASK]??PROMPT_DEFAULTS[PROMPT_TYPE_ASK]
// replace params
prompt = prompt.replaceAll('{{language}}', summarizeLanguage.name)
prompt = prompt.replaceAll('{{title}}', title??'')
prompt = prompt.replaceAll('{{segment}}', segment.text)
prompt = prompt.replaceAll('{{question}}', question)
const taskDef: TaskDef = {
type: envData.aiType === 'gemini'?'geminiChatComplete':'chatComplete',
serverUrl: envData.serverUrl,
data: envData.aiType === 'gemini'
?{
contents: [
{
parts: [
{
text: prompt
}
]
}
],
generationConfig: {
maxOutputTokens: 2048
}
}
:{
model: getModel(envData),
messages: [
{
role: 'user',
content: prompt,
}
],
temperature: 0.5,
n: 1,
stream: false,
},
extra: {
type: 'ask',
// startIdx: segment.startIdx,
apiKey: envData.apiKey,
geminiApiKey: envData.geminiApiKey,
askId: id,
}
}
console.debug('addAskTask', taskDef)
dispatch(mergeAskInfo({
id,
status: 'pending'
}))
const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef})
dispatch(addTaskId(task.id))
}
}, [dispatch, envData, summarizeLanguage.name, title])
const handleTranslate = useMemoizedFn((task: Task, content: string) => {
let map: {[key: string]: string} = {}
try {
content = extractJsonObject(content)
map = JSON.parse(content)
} catch (e) {
console.debug(e)
}
const {startIdx, size} = task.def.extra
if (startIdx != null) {
const result: { [key: number]: TransResult } = {}
for (let i = 0; i < size; i++) {
const item = map[(i + 1)+'']
if (item) {
result[startIdx + i] = {
// idx: startIdx + i,
code: '200',
data: item,
}
} else {
result[startIdx + i] = {
// idx: startIdx + i,
code: '500',
}
}
}
dispatch(addTransResults(result))
console.debug('addTransResults', map, size)
}
})
const handleSummarize = useMemoizedFn((task: Task, content?: string) => {
const summaryType = task.def.extra.summaryType
content = summaryType === 'brief'?extractJsonObject(content??''):extractJsonArray(content??'')
let obj
try {
obj = JSON.parse(content)
} catch (e) {
task.error = 'failed'
}
dispatch(setSummaryContent({
segmentStartIdx: task.def.extra.startIdx,
type: summaryType,
content: obj,
}))
dispatch(setSummaryStatus({segmentStartIdx: task.def.extra.startIdx, type: summaryType, status: 'done'}))
dispatch(setSummaryError({segmentStartIdx: task.def.extra.startIdx, type: summaryType, error: task.error}))
console.debug('setSummary', task.def.extra.startIdx, summaryType, obj, task.error)
})
const handleAsk = useMemoizedFn((task: Task, content?: string) => {
dispatch(mergeAskInfo({
id: task.def.extra.askId,
content,
status: 'done',
error: task.error,
}))
console.debug('setAsk', content, task.error)
})
const getTask = useCallback(async (taskId: string) => {
const taskResp = await chrome.runtime.sendMessage({type: 'getTask', taskId})
if (taskResp.code === 'ok') {
console.debug('getTask', taskResp.task)
const task: Task = taskResp.task
const taskType: string | undefined = task.def.extra?.type
const content = envData.aiType === 'gemini'?task.resp?.candidates[0]?.content?.parts[0]?.text?.trim():task.resp?.choices?.[0]?.message?.content?.trim()
if (task.status === 'done') {
// 异常提示
if (task.error) {
toast.error(task.error)
}
// 删除任务
dispatch(delTaskId(taskId))
// 处理结果
if (taskType === 'translate') { // 翻译
handleTranslate(task, content)
} else if (taskType === 'summarize') { // 总结
handleSummarize(task, content)
} else if (taskType === 'ask') { // 总结
handleAsk(task, content)
}
}
} else {
dispatch(delTaskId(taskId))
}
}, [dispatch, envData.aiType, handleAsk, handleSummarize, handleTranslate])
return {getFetch, getTask, addTask, addSummarizeTask, addAskTask}
}
export default useTranslate