You've already forked bilibili-subtitle
364 lines
12 KiB
TypeScript
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
|