You've already forked bilibili-subtitle
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
75235883a5 | ||
![]() |
aeac55cb6b | ||
![]() |
cf1bd97dec | ||
![]() |
915cd8022d | ||
![]() |
e7f3eef6dc | ||
![]() |
71ac909855 | ||
![]() |
60697769bb | ||
![]() |
c40338a5f5 | ||
![]() |
13e90c1ab7 | ||
![]() |
9b2d620cdf |
@@ -27,6 +27,13 @@
|
||||
|
||||
安装扩展后,在哔哩哔哩网站观看视频时,视频右侧会显示字幕列表面板。
|
||||
|
||||
### 使用本地Ollama模型
|
||||
如果你使用本地Ollama模型,需要配置环境变量:`OLLAMA_ORIGINS=chrome-extension://bciglihaegkdhoogebcdblfhppoilclp`,否则访问会出现403错误。
|
||||
|
||||
然后在插件配置里,apiKey随便填一个,服务器地址填`http://localhost:11434`,模型选自定义,然后填入自定义模型名如`llama2`。
|
||||
|
||||
但是测试发现llama2 7b模型比较弱,无法返回需要的json格式,因此总结很可能会无法解析响应而报错(但提问功能不需要解析响应格式,因此没问题)。
|
||||
|
||||
## 交流联系
|
||||
|
||||
QQ群:194536885
|
||||
|
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"name": "哔哩哔哩字幕列表",
|
||||
"description": "显示B站视频的字幕列表,可点击跳转与下载字幕,并支持翻译和总结字幕!",
|
||||
"version": "1.9.0",
|
||||
"version": "1.9.3",
|
||||
"manifest_version": 3,
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://localhost/*",
|
||||
"http://127.0.0.1/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "src/chrome/background.ts"
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "bilibili-subtitle",
|
||||
"version": "1.9.0",
|
||||
"version": "1.9.3",
|
||||
"type": "module",
|
||||
"description": "哔哩哔哩字幕列表",
|
||||
"main": "index.js",
|
||||
|
BIN
public/my-article-summarizer.png
Normal file
BIN
public/my-article-summarizer.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
@@ -43,6 +43,7 @@ import {getSummarize} from '../util/biz_util'
|
||||
import {openUrl} from '@kky002/kky-util'
|
||||
import Markdown from '../components/Markdown'
|
||||
import {random} from 'lodash-es'
|
||||
import useKeyService from '../hooks/useKeyService'
|
||||
|
||||
const Body = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -207,6 +208,9 @@ const Body = () => {
|
||||
dispatch(setAskFold(!askFold))
|
||||
}, [askFold, dispatch])
|
||||
|
||||
// service
|
||||
useKeyService()
|
||||
|
||||
// 自动滚动
|
||||
useEffect(() => {
|
||||
if (checkAutoScroll && curOffsetTop && autoScroll && !needScroll) {
|
||||
@@ -304,6 +308,13 @@ const Body = () => {
|
||||
segmentIdx={segmentIdx} bodyRef={bodyRef}/>)}
|
||||
|
||||
{/* tip */}
|
||||
<div className='text-sm font-semibold text-center'>快捷键提示</div>
|
||||
<ul className='list-disc text-sm desc pl-5'>
|
||||
<li>单击字幕跳转,双击字幕跳转+切换暂停。</li>
|
||||
<li>alt+单击字幕复制单条字幕。</li>
|
||||
<li>上下方向键来移动当前字幕(可先点击字幕使焦点在字幕列表内)。</li>
|
||||
</ul>
|
||||
|
||||
{/* <div className='flex flex-col items-center text-center pt-1 pb-2'> */}
|
||||
{/* <div className='font-semibold text-accent'>💡<span className='underline underline-offset-4'>提示</span>💡</div> */}
|
||||
{/* <div className='text-sm desc px-2'>可以尝试将<span className='text-amber-600 font-semibold'>概览</span>生成的内容粘贴到<span */}
|
||||
@@ -313,8 +324,8 @@ const Body = () => {
|
||||
{/* onClick={onCopy}>点击复制生成的{SUMMARIZE_TYPES[curSummaryType].name}<RiFileCopy2Line/> */}
|
||||
{/* </button>} */}
|
||||
{/* </div> */}
|
||||
{((infos == null) || infos.length === 0) && <div className='flex flex-col'>
|
||||
<div className='flex flex-col items-center text-center py-2 mx-4'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex flex-col items-center text-center py-2 mx-4 border-t border-t-base-300'>
|
||||
<div className='font-semibold text-accent flex items-center gap-1'><img src='/bibigpt.png'
|
||||
alt='BibiGPT logo'
|
||||
className='w-8 h-8'/>BibiGPT
|
||||
@@ -349,7 +360,8 @@ const Body = () => {
|
||||
e.preventDefault()
|
||||
openUrl('https://microsoftedge.microsoft.com/addons/detail/galeejdehabppfgooagmkclpppnbccpc')
|
||||
}} className='link text-sm text-accent'>Edge商店</a>
|
||||
<a title='Crx搜搜(国内可访问)' href='https://www.crxsoso.com/webstore/detail/fiaeclpicddpifeflpmlgmbjgaedladf'
|
||||
<a title='Crx搜搜(国内可访问)'
|
||||
href='https://www.crxsoso.com/webstore/detail/fiaeclpicddpifeflpmlgmbjgaedladf'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openUrl('https://www.crxsoso.com/webstore/detail/fiaeclpicddpifeflpmlgmbjgaedladf')
|
||||
@@ -357,26 +369,26 @@ const Body = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col items-center text-center py-2 mx-4 border-t border-t-base-300'>
|
||||
<div className='font-semibold text-accent flex items-center gap-1'><img src='/immersive-summary.png'
|
||||
alt='Immersive Summary logo'
|
||||
className='w-8 h-8'/>Immersive Summary
|
||||
<div className='font-semibold text-accent flex items-center gap-1'><img src='/my-article-summarizer.png'
|
||||
alt='My Article Summarizer logo'
|
||||
className='w-8 h-8'/>My Article Summarizer
|
||||
</div>
|
||||
<div className='text-sm px-2 desc'>沉浸式总结,多种方式总结网页文章。</div>
|
||||
<div className='text-sm px-2 desc'>网页文章总结(有每日免费额度,无需apikey)。</div>
|
||||
<div className='flex gap-2'>
|
||||
<a title='Chrome商店' href='https://chromewebstore.google.com/detail/mcijpllinkhflgpkggimnafkbmpiijah'
|
||||
<a title='Chrome商店' href='https://chromewebstore.google.com/detail/my-article-summarizer/nanlpakfialleijdidafldapoifndngn'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openUrl('https://chromewebstore.google.com/detail/mcijpllinkhflgpkggimnafkbmpiijah')
|
||||
openUrl('https://chromewebstore.google.com/detail/my-article-summarizer/nanlpakfialleijdidafldapoifndngn')
|
||||
}} className='link text-sm text-accent'>Chrome商店</a>
|
||||
<a title='Crx搜搜(国内可访问)'
|
||||
href='https://www.crxsoso.com/webstore/detail/mcijpllinkhflgpkggimnafkbmpiijah'
|
||||
href='https://www.crxsoso.com/webstore/detail/nanlpakfialleijdidafldapoifndngn'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openUrl('https://www.crxsoso.com/webstore/detail/mcijpllinkhflgpkggimnafkbmpiijah')
|
||||
openUrl('https://www.crxsoso.com/webstore/detail/nanlpakfialleijdidafldapoifndngn')
|
||||
}} className='link text-sm text-accent'>Crx搜搜(国内可访问)</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* recommend */}
|
||||
@@ -404,11 +416,11 @@ const Body = () => {
|
||||
{recommendIdx === 2 && <div className='flex items-center gap-1 rounded shadow-sm bg-base-200/10'>
|
||||
<a className='link link-accent link-hover font-semibold text-sm flex items-center' onClick={(e) => {
|
||||
e.preventDefault()
|
||||
openUrl('https://chromewebstore.google.com/detail/mcijpllinkhflgpkggimnafkbmpiijah')
|
||||
}}><img src='/immersive-summary.png'
|
||||
alt='Immersive Summary logo'
|
||||
className='w-8 h-8'/>Immersive Summary</a>
|
||||
<span className='text-sm desc'>沉浸式总结网页文章。</span>
|
||||
openUrl('https://chromewebstore.google.com/detail/nanlpakfialleijdidafldapoifndngn')
|
||||
}}><img src='/my-article-summarizer.png'
|
||||
alt='My Article Summarizer logo'
|
||||
className='w-8 h-8'/>My Article Summarizer</a>
|
||||
<span className='text-sm desc'>网页文章总结。</span>
|
||||
</div>}
|
||||
{recommendIdx === 3 && <div className='flex items-center gap-1 rounded shadow-sm bg-base-200/10'>
|
||||
<a className='link link-accent link-hover font-semibold text-sm flex items-center' onClick={(e) => {
|
||||
@@ -417,7 +429,8 @@ const Body = () => {
|
||||
}}><img src='/openai-up.ico'
|
||||
alt='Openai Up logo'
|
||||
className='w-8 h-8'/>Openai代理</a>
|
||||
<span className='text-sm desc flex items-center'>目前价格不到官方的6折<FaGripfire className='text-amber-600'/></span>
|
||||
<span className='text-sm desc flex items-center'>目前价格不到官方的6折<FaGripfire
|
||||
className='text-amber-600'/></span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -9,8 +9,9 @@ const CompactSegmentItem = (props: {
|
||||
isIn: boolean
|
||||
last: boolean
|
||||
moveCallback: (event: any) => void
|
||||
move2Callback: (event: any) => void
|
||||
}) => {
|
||||
const {item, idx, last, isIn, moveCallback} = props
|
||||
const {item, idx, last, isIn, moveCallback, move2Callback} = props
|
||||
const transResult = useAppSelector(state => state.env.transResults[idx])
|
||||
const envData = useAppSelector(state => state.env.envData)
|
||||
const fontSize = useAppSelector(state => state.env.envData.fontSize)
|
||||
@@ -19,7 +20,7 @@ const CompactSegmentItem = (props: {
|
||||
const display = useMemo(() => getDisplay(envData.transDisplay, item.content, transText), [envData.transDisplay, item.content, transText])
|
||||
|
||||
return <div className={classNames('inline', fontSize === 'large'?'text-sm':'text-xs')}>
|
||||
<span className={'pl-1 pr-0.5 py-0.5 cursor-pointer rounded-sm hover:bg-base-200'} onClick={moveCallback}>
|
||||
<span className={'pl-1 pr-0.5 py-0.5 cursor-pointer rounded-sm hover:bg-base-200'} onClick={moveCallback} onDoubleClick={move2Callback}>
|
||||
<text className={classNames('font-medium', isIn ? 'text-primary underline' : '')}>{display.main}</text>
|
||||
{display.sub && <text className='desc'>({display.sub})</text>}</span>
|
||||
<span className='text-base-content/75'>{!last && ','}</span>
|
||||
|
@@ -9,8 +9,9 @@ const NormalSegmentItem = (props: {
|
||||
idx: number
|
||||
isIn: boolean
|
||||
moveCallback: (event: any) => void
|
||||
move2Callback: (event: any) => void
|
||||
}) => {
|
||||
const {item, idx, isIn, moveCallback} = props
|
||||
const {item, idx, isIn, moveCallback, move2Callback} = props
|
||||
const transResult = useAppSelector(state => state.env.transResults[idx])
|
||||
const envData = useAppSelector(state => state.env.envData)
|
||||
const fontSize = useAppSelector(state => state.env.envData.fontSize)
|
||||
@@ -19,7 +20,7 @@ const NormalSegmentItem = (props: {
|
||||
const display = useMemo(() => getDisplay(envData.transDisplay, item.content, transText), [envData.transDisplay, item.content, transText])
|
||||
|
||||
return <div className={classNames('flex py-0.5 cursor-pointer rounded-sm hover:bg-base-200', fontSize === 'large'?'text-sm':'text-xs')}
|
||||
onClick={moveCallback}>
|
||||
onClick={moveCallback} onDoubleClick={move2Callback}>
|
||||
<div className='desc w-[66px] flex justify-center'>{formatTime(item.from)}</div>
|
||||
<div className={'flex-1'}>
|
||||
<div className={classNames('font-medium', isIn ? 'text-primary underline' : '')}>{display.main}</div>
|
||||
|
@@ -46,7 +46,7 @@ const SummarizeItemOverview = (props: {
|
||||
if (event.altKey) { // 复制
|
||||
navigator.clipboard.writeText(overviewItem.key).catch(console.error)
|
||||
} else {
|
||||
move(time)
|
||||
move(time, false)
|
||||
}
|
||||
}, [overviewItem.key, move, time])
|
||||
|
||||
|
@@ -33,7 +33,15 @@ const SegmentItem = (props: {
|
||||
if (event.altKey) { // 复制
|
||||
navigator.clipboard.writeText(item.content).catch(console.error)
|
||||
} else {
|
||||
move(item.from)
|
||||
move(item.from, false)
|
||||
}
|
||||
}, [item.content, item.from, move])
|
||||
|
||||
const move2Callback = useCallback((event: any) => {
|
||||
if (event.altKey) { // 复制
|
||||
navigator.clipboard.writeText(item.content).catch(console.error)
|
||||
} else {
|
||||
move(item.from, true)
|
||||
}
|
||||
}, [item.content, item.from, move])
|
||||
|
||||
@@ -63,6 +71,7 @@ const SegmentItem = (props: {
|
||||
isIn={isIn}
|
||||
last={last}
|
||||
moveCallback={moveCallback}
|
||||
move2Callback={move2Callback}
|
||||
/>
|
||||
:
|
||||
<NormalSegmentItem
|
||||
@@ -70,6 +79,7 @@ const SegmentItem = (props: {
|
||||
idx={idx}
|
||||
isIn={isIn}
|
||||
moveCallback={moveCallback}
|
||||
move2Callback={move2Callback}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
|
@@ -3,6 +3,8 @@ import {setEnvData, setPage} from '../redux/envReducer'
|
||||
import {useAppDispatch, useAppSelector} from '../hooks/redux'
|
||||
import {
|
||||
ASK_ENABLED_DEFAULT,
|
||||
CUSTOM_MODEL_TOKENS,
|
||||
DEFAULT_SERVER_URL_OPENAI,
|
||||
GEMINI_TOKENS,
|
||||
HEADER_HEIGHT,
|
||||
LANGUAGE_DEFAULT,
|
||||
@@ -56,7 +58,7 @@ const FormItem = (props: {
|
||||
const Settings = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const envData = useAppSelector(state => state.env.envData)
|
||||
// const {value: autoExpandValue, onChange: setAutoExpandValue} = useEventChecked(envData.autoExpand)
|
||||
const {value: autoExpandValue, onChange: setAutoExpandValue} = useEventChecked(envData.autoExpand)
|
||||
// const {value: autoScrollValue, onChange: setAutoScrollValue} = useEventChecked(envData.autoScroll)
|
||||
const {value: translateEnableValue, onChange: setTranslateEnableValue} = useEventChecked(envData.translateEnable)
|
||||
const {value: summarizeEnableValue, onChange: setSummarizeEnableValue} = useEventChecked(envData.summarizeEnable)
|
||||
@@ -69,6 +71,8 @@ const Settings = () => {
|
||||
const [geminiApiKeyValue, { onChange: onChangeGeminiApiKeyValue }] = useEventTarget({initialValue: envData.geminiApiKey??''})
|
||||
const [languageValue, { onChange: onChangeLanguageValue }] = useEventTarget({initialValue: envData.language??LANGUAGE_DEFAULT})
|
||||
const [modelValue, { onChange: onChangeModelValue }] = useEventTarget({initialValue: envData.model??MODEL_DEFAULT})
|
||||
const [customModelValue, { onChange: onChangeCustomModelValue }] = useEventTarget({initialValue: envData.customModel})
|
||||
const [customModelTokensValue, setCustomModelTokensValue] = useState(envData.customModelTokens)
|
||||
const [summarizeLanguageValue, { onChange: onChangeSummarizeLanguageValue }] = useEventTarget({initialValue: envData.summarizeLanguage??SUMMARIZE_LANGUAGE_DEFAULT})
|
||||
const [hideOnDisableAutoTranslateValue, setHideOnDisableAutoTranslateValue] = useState(envData.hideOnDisableAutoTranslate)
|
||||
const [themeValue, setThemeValue] = useState(envData.theme)
|
||||
@@ -109,11 +113,13 @@ const Settings = () => {
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
dispatch(setEnvData({
|
||||
// autoExpand: autoExpandValue,
|
||||
autoExpand: autoExpandValue,
|
||||
aiType: aiTypeValue,
|
||||
apiKey: apiKeyValue,
|
||||
serverUrl: serverUrlValue,
|
||||
model: modelValue,
|
||||
customModel: customModelValue,
|
||||
customModelTokens: customModelTokensValue,
|
||||
geminiApiKey: geminiApiKeyValue,
|
||||
translateEnable: translateEnableValue,
|
||||
language: languageValue,
|
||||
@@ -133,7 +139,7 @@ const Settings = () => {
|
||||
}))
|
||||
dispatch(setPage(PAGE_MAIN))
|
||||
toast.success('保存成功')
|
||||
}, [dispatch, aiTypeValue, apiKeyValue, serverUrlValue, modelValue, geminiApiKeyValue, translateEnableValue, languageValue, hideOnDisableAutoTranslateValue, themeValue, transDisplayValue, summarizeEnableValue, summarizeFloatValue, summarizeLanguageValue, wordsValue, fetchAmountValue, fontSizeValue, promptsValue, searchEnabledValue, cnSearchEnabledValue, askEnabledValue])
|
||||
}, [dispatch, autoExpandValue, aiTypeValue, apiKeyValue, serverUrlValue, modelValue, customModelValue, customModelTokensValue, geminiApiKeyValue, translateEnableValue, languageValue, hideOnDisableAutoTranslateValue, themeValue, transDisplayValue, summarizeEnableValue, summarizeFloatValue, summarizeLanguageValue, wordsValue, fetchAmountValue, fontSizeValue, promptsValue, searchEnabledValue, cnSearchEnabledValue, askEnabledValue])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
dispatch(setPage(PAGE_MAIN))
|
||||
@@ -192,10 +198,10 @@ const Settings = () => {
|
||||
}}>
|
||||
<div className="flex flex-col gap-3 p-2">
|
||||
<Section title='通用配置'>
|
||||
{/* <FormItem title='自动展开' htmlFor='autoExpand' tip='是否视频有字幕时自动展开字幕列表'> */}
|
||||
{/* <input id='autoExpand' type='checkbox' className='toggle toggle-primary' checked={autoExpandValue} */}
|
||||
{/* onChange={setAutoExpandValue}/> */}
|
||||
{/* </FormItem> */}
|
||||
<FormItem title='自动展开' htmlFor='autoExpand' tip='是否视频有字幕时自动展开字幕列表'>
|
||||
<input id='autoExpand' type='checkbox' className='toggle toggle-primary' checked={autoExpandValue}
|
||||
onChange={setAutoExpandValue}/>
|
||||
</FormItem>
|
||||
<FormItem title='主题'>
|
||||
<div className="btn-group">
|
||||
<button onClick={onSelTheme1} className={classNames('btn btn-xs no-animation', (!themeValue || themeValue === 'system')?'btn-active':'')}>系统</button>
|
||||
@@ -224,7 +230,7 @@ const Settings = () => {
|
||||
</FormItem>
|
||||
<FormItem title='服务器' htmlFor='serverUrl'>
|
||||
<input id='serverUrl' type='text' className='input input-sm input-bordered w-full'
|
||||
placeholder='默认使用官方地址' value={serverUrlValue}
|
||||
placeholder={DEFAULT_SERVER_URL_OPENAI} value={serverUrlValue}
|
||||
onChange={e => setServerUrlValue(e.target.value)}/>
|
||||
</FormItem>
|
||||
<div>
|
||||
@@ -233,7 +239,7 @@ const Settings = () => {
|
||||
<div>官方网址:<a className='link link-primary' href='https://platform.openai.com/' target='_blank'
|
||||
rel="noreferrer">点击访问</a></div>
|
||||
<div>服务器地址:<a className='link link-primary'
|
||||
onClick={() => setServerUrlValue('https://api.openai.com')}
|
||||
onClick={() => setServerUrlValue(DEFAULT_SERVER_URL_OPENAI)}
|
||||
rel='noreferrer'>点击设置</a></div>
|
||||
<div className='flex justify-center font-semibold'>【第三方代理】</div>
|
||||
<div>代理网址:<a className='link link-primary' href='https://api.openai-up.com/register?aff=varM'
|
||||
@@ -251,31 +257,14 @@ const Settings = () => {
|
||||
{MODELS.map(model => <option key={model.code} value={model.code}>{model.name}</option>)}
|
||||
</select>
|
||||
</FormItem>
|
||||
<div className='flex justify-center'>
|
||||
<a className='link text-xs'
|
||||
onClick={togglePromptsFold}>{promptsFold ? '点击查看提示词' : '点击折叠提示词'}</a>
|
||||
</div>
|
||||
{!promptsFold && <div>
|
||||
{PROMPT_TYPES.map((item, idx) => <FormItem key={item.type} title={<div>
|
||||
<div>{item.name}</div>
|
||||
<div className='link text-xs' onClick={() => {
|
||||
setPromptsValue({
|
||||
...promptsValue,
|
||||
// @ts-expect-error
|
||||
[item.type]: PROMPT_DEFAULTS[item.type] ?? ''
|
||||
})
|
||||
}}>点击填充默认
|
||||
</div>
|
||||
</div>} htmlFor={`prompt-${item.type}`}>
|
||||
<textarea id={`prompt-${item.type}`} className='mt-2 textarea input-bordered w-full'
|
||||
placeholder='留空使用默认提示词' value={promptsValue[item.type] ?? ''} onChange={(e) => {
|
||||
setPromptsValue({
|
||||
...promptsValue,
|
||||
[item.type]: e.target.value
|
||||
})
|
||||
}}/>
|
||||
</FormItem>)}
|
||||
</div>}
|
||||
{modelValue === 'custom' && <FormItem title='模型名' htmlFor='customModel'>
|
||||
<input id='customModel' type='text' className='input input-sm input-bordered w-full' placeholder='llama2'
|
||||
value={customModelValue} onChange={onChangeCustomModelValue}/>
|
||||
</FormItem>}
|
||||
{modelValue === 'custom' && <FormItem title='Token上限' htmlFor='customModelTokens'>
|
||||
<input id='customModelTokens' type='number' className='input input-sm input-bordered w-full' placeholder={''+CUSTOM_MODEL_TOKENS}
|
||||
value={customModelTokensValue} onChange={e => setCustomModelTokensValue(e.target.value?parseInt(e.target.value):undefined)}/>
|
||||
</FormItem>}
|
||||
</Section>}
|
||||
|
||||
{aiTypeValue === 'gemini' && <Section title='gemini配置'>
|
||||
@@ -291,33 +280,30 @@ const Settings = () => {
|
||||
<div className='text-xs text-error flex items-center'><IoWarning className='text-sm text-warning'/>谷歌模型安全要求比较高,有些视频可能无法生成总结!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-center'>
|
||||
<a className='link text-xs'
|
||||
onClick={togglePromptsFold}>{promptsFold ? '点击查看提示词' : '点击折叠提示词'}</a>
|
||||
</div>
|
||||
{!promptsFold && <div>
|
||||
{PROMPT_TYPES.map((item, idx) => <FormItem key={item.type} title={<div>
|
||||
<div>{item.name}</div>
|
||||
<div className='link text-xs' onClick={() => {
|
||||
setPromptsValue({
|
||||
...promptsValue,
|
||||
// @ts-expect-error
|
||||
[item.type]: PROMPT_DEFAULTS[item.type] ?? ''
|
||||
})
|
||||
}}>点击填充默认
|
||||
</div>
|
||||
</div>} htmlFor={`prompt-${item.type}`}>
|
||||
<textarea id={`prompt-${item.type}`} className='mt-2 textarea input-bordered w-full'
|
||||
placeholder='留空使用默认提示词' value={promptsValue[item.type] ?? ''} onChange={(e) => {
|
||||
setPromptsValue({
|
||||
...promptsValue,
|
||||
[item.type]: e.target.value
|
||||
})
|
||||
}}/>
|
||||
</FormItem>)}
|
||||
</div>}
|
||||
</Section>}
|
||||
|
||||
<Section title='提示词配置'>
|
||||
{PROMPT_TYPES.map((item, idx) => <FormItem key={item.type} title={<div>
|
||||
<div>{item.name}</div>
|
||||
<div className='link text-xs' onClick={() => {
|
||||
setPromptsValue({
|
||||
...promptsValue,
|
||||
// @ts-expect-error
|
||||
[item.type]: PROMPT_DEFAULTS[item.type] ?? ''
|
||||
})
|
||||
}}>点击填充默认
|
||||
</div>
|
||||
</div>} htmlFor={`prompt-${item.type}`}>
|
||||
<textarea id={`prompt-${item.type}`} className='mt-2 textarea input-bordered w-full'
|
||||
placeholder='留空使用默认提示词' value={promptsValue[item.type] ?? ''} onChange={(e) => {
|
||||
setPromptsValue({
|
||||
...promptsValue,
|
||||
[item.type]: e.target.value
|
||||
})
|
||||
}}/>
|
||||
</FormItem>)}
|
||||
</Section>
|
||||
|
||||
<Section title={<div className='flex items-center'>
|
||||
翻译配置
|
||||
{!apiKeySetted && <div className='tooltip tooltip-right ml-1' data-tip='未设置ApiKey无法使用'>
|
||||
@@ -329,7 +315,8 @@ const Settings = () => {
|
||||
onChange={setTranslateEnableValue}/>
|
||||
</FormItem>
|
||||
<FormItem title='目标语言' htmlFor='language'>
|
||||
<select id='language' className="select select-sm select-bordered" value={languageValue} onChange={onChangeLanguageValue}>
|
||||
<select id='language' className="select select-sm select-bordered" value={languageValue}
|
||||
onChange={onChangeLanguageValue}>
|
||||
{LANGUAGES.map(language => <option key={language.code} value={language.code}>{language.name}</option>)}
|
||||
</select>
|
||||
</FormItem>
|
||||
|
@@ -158,6 +158,9 @@ window.addEventListener("message", (event) => {
|
||||
const video = getVideoElement()
|
||||
if (video) {
|
||||
video.currentTime = data.time
|
||||
if (data.togglePause) {
|
||||
video.paused ? video.play() : video.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import {DEFAULT_SERVER_URL_OPENAI} from '../const'
|
||||
|
||||
const getServerUrl = (serverUrl?: string) => {
|
||||
if (!serverUrl) {
|
||||
return 'https://api.openai.com'
|
||||
return DEFAULT_SERVER_URL_OPENAI
|
||||
}
|
||||
if (serverUrl.endsWith('/')) {
|
||||
serverUrl = serverUrl.slice(0, -1)
|
||||
|
@@ -169,7 +169,8 @@ export const SUMMARIZE_THRESHOLD = 100
|
||||
export const SUMMARIZE_LANGUAGE_DEFAULT = 'cn'
|
||||
export const SUMMARIZE_ALL_THRESHOLD = 5
|
||||
export const ASK_ENABLED_DEFAULT = true
|
||||
export const SERVER_URL_OPENAI = 'https://api.openai.com'
|
||||
export const DEFAULT_SERVER_URL_OPENAI = 'https://api.openai.com'
|
||||
export const CUSTOM_MODEL_TOKENS = 16385
|
||||
|
||||
export const MODELS = [{
|
||||
code: 'gpt-3.5-turbo',
|
||||
@@ -183,6 +184,9 @@ export const MODELS = [{
|
||||
code: 'gpt-3.5-turbo-1106',
|
||||
name: 'gpt-3.5-turbo-1106',
|
||||
tokens: 16385,
|
||||
}, {
|
||||
code: 'custom',
|
||||
name: '自定义',
|
||||
}]
|
||||
export const GEMINI_TOKENS = 32768
|
||||
export const MODEL_DEFAULT = MODELS[1].code
|
||||
|
59
src/hooks/useKeyService.ts
Normal file
59
src/hooks/useKeyService.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {useEffect} from 'react'
|
||||
import {useMemoizedFn} from 'ahooks/es'
|
||||
import {useAppSelector} from './redux'
|
||||
import useSubtitle from './useSubtitle'
|
||||
|
||||
const useKeyService = () => {
|
||||
const curIdx = useAppSelector(state => state.env.curIdx)
|
||||
const data = useAppSelector(state => state.env.data)
|
||||
const {move} = useSubtitle()
|
||||
|
||||
const onKeyDown = useMemoizedFn((e: KeyboardEvent) => {
|
||||
// 有按其他控制键时,不触发
|
||||
if (e.ctrlKey || e.metaKey || e.shiftKey) {
|
||||
return
|
||||
}
|
||||
|
||||
let cursorInInput = false
|
||||
if (document.activeElement != null) {
|
||||
const tagName = document.activeElement.tagName
|
||||
if (tagName === 'INPUT' || tagName === 'TEXTAREA') {
|
||||
cursorInInput = true
|
||||
}
|
||||
}
|
||||
let prevent = false
|
||||
|
||||
// up arrow
|
||||
if (e.key === 'ArrowUp') {
|
||||
if (curIdx && (data != null) && !cursorInInput) {
|
||||
prevent = true
|
||||
const newCurIdx = Math.max(curIdx - 1, 0)
|
||||
move(data.body[newCurIdx].from, false)
|
||||
}
|
||||
}
|
||||
// down arrow
|
||||
if (e.key === 'ArrowDown') {
|
||||
if (curIdx !== undefined && (data != null) && !cursorInInput) {
|
||||
prevent = true
|
||||
const newCurIdx = Math.min(curIdx + 1, data.body.length - 1)
|
||||
move(data.body[newCurIdx].from, false)
|
||||
}
|
||||
}
|
||||
|
||||
// 阻止默认事件
|
||||
if (prevent) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
})
|
||||
|
||||
// 检测快捷键
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
}
|
||||
}, [onKeyDown])
|
||||
}
|
||||
|
||||
export default useKeyService
|
@@ -5,8 +5,8 @@ import {setNeedScroll} from '../redux/envReducer'
|
||||
const useSubtitle = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const move = useCallback((time: number) => {
|
||||
window.parent.postMessage({type: 'move', time}, '*')
|
||||
const move = useCallback((time: number, togglePause: boolean) => {
|
||||
window.parent.postMessage({type: 'move', time, togglePause}, '*')
|
||||
}, [])
|
||||
|
||||
const scrollIntoView = useCallback((ref: React.RefObject<HTMLDivElement>) => {
|
||||
|
@@ -15,18 +15,9 @@ import {
|
||||
setUrl,
|
||||
} from '../redux/envReducer'
|
||||
import {EventBusContext} from '../Router'
|
||||
import {
|
||||
EVENT_EXPAND,
|
||||
GEMINI_TOKENS,
|
||||
MODEL_DEFAULT,
|
||||
MODEL_MAP,
|
||||
TOTAL_HEIGHT_MAX,
|
||||
TOTAL_HEIGHT_MIN,
|
||||
WORDS_MIN,
|
||||
WORDS_RATE
|
||||
} from '../const'
|
||||
import {EVENT_EXPAND, GEMINI_TOKENS, TOTAL_HEIGHT_MAX, TOTAL_HEIGHT_MIN, WORDS_MIN, WORDS_RATE} from '../const'
|
||||
import {useInterval} from 'ahooks'
|
||||
import {getWholeText} from '../util/biz_util'
|
||||
import {getModelMaxTokens, getWholeText} from '../util/biz_util'
|
||||
|
||||
/**
|
||||
* Service是单例,类似后端的服务概念
|
||||
@@ -100,20 +91,20 @@ const useSubtitleService = () => {
|
||||
|
||||
// 有数据时自动展开
|
||||
useEffect(() => {
|
||||
if (infos != null) {
|
||||
if ((data != null) && data.body.length > 0) {
|
||||
eventBus.emit({
|
||||
type: EVENT_EXPAND
|
||||
})
|
||||
}
|
||||
}, [eventBus, infos])
|
||||
}, [data, eventBus, infos])
|
||||
|
||||
// 当前未展示 & 未折叠 & 有列表 => 展示第一个
|
||||
// 当前未展示 & (未折叠 | 自动展开) & 有列表 => 展示第一个
|
||||
useEffect(() => {
|
||||
if (!curInfo && !fold && (infos != null) && infos.length > 0) {
|
||||
if (!curInfo && (!fold || (envReady && envData.autoExpand)) && (infos != null) && infos.length > 0) {
|
||||
dispatch(setCurInfo(infos[0]))
|
||||
dispatch(setCurFetched(false))
|
||||
}
|
||||
}, [curInfo, dispatch, envReady, fold, infos])
|
||||
}, [curInfo, dispatch, envData.autoExpand, envReady, fold, infos])
|
||||
// 获取
|
||||
useEffect(() => {
|
||||
if (curInfo && !curFetched) {
|
||||
@@ -172,7 +163,7 @@ const useSubtitleService = () => {
|
||||
if (envData.aiType === 'gemini') {
|
||||
size = GEMINI_TOKENS*WORDS_RATE
|
||||
} else {
|
||||
size = (MODEL_MAP[envData.model??MODEL_DEFAULT]?.tokens??4000)*WORDS_RATE
|
||||
size = getModelMaxTokens(envData)*WORDS_RATE
|
||||
}
|
||||
}
|
||||
size = Math.max(size, WORDS_MIN)
|
||||
@@ -209,7 +200,7 @@ const useSubtitleService = () => {
|
||||
}
|
||||
}
|
||||
dispatch(setSegments(segments))
|
||||
}, [data?.body, dispatch, envData.aiType, envData.model, envData.summarizeEnable, envData.words])
|
||||
}, [data?.body, dispatch, envData])
|
||||
|
||||
// 每秒更新当前视频时间
|
||||
useInterval(() => {
|
||||
|
@@ -16,7 +16,6 @@ import {
|
||||
import {
|
||||
LANGUAGE_DEFAULT,
|
||||
LANGUAGES_MAP,
|
||||
MODEL_DEFAULT,
|
||||
PROMPT_DEFAULTS,
|
||||
PROMPT_TYPE_ASK,
|
||||
PROMPT_TYPE_TRANSLATE,
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
} from '../const'
|
||||
import toast from 'react-hot-toast'
|
||||
import {useMemoizedFn} from 'ahooks/es'
|
||||
import {extractJsonArray, extractJsonObject} from '../util/biz_util'
|
||||
import {extractJsonArray, extractJsonObject, getModel} from '../util/biz_util'
|
||||
import {formatTime} from '../util/util'
|
||||
|
||||
const useTranslate = () => {
|
||||
@@ -104,7 +103,7 @@ const useTranslate = () => {
|
||||
}
|
||||
}
|
||||
:{
|
||||
model: envData.model??MODEL_DEFAULT,
|
||||
model: getModel(envData),
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -137,7 +136,7 @@ const useTranslate = () => {
|
||||
dispatch(addTaskId(task.id))
|
||||
}
|
||||
}
|
||||
}, [data?.body, envData.fetchAmount, envData.prompts, envData.aiType, envData.serverUrl, envData.model, envData.apiKey, envData.geminiApiKey, language.name, title, dispatch])
|
||||
}, [data?.body, envData, language.name, title, dispatch])
|
||||
|
||||
const addSummarizeTask = useCallback(async (type: SummaryType, segment: Segment) => {
|
||||
if (segment.text.length >= SUMMARIZE_THRESHOLD) {
|
||||
@@ -173,7 +172,7 @@ const useTranslate = () => {
|
||||
}
|
||||
}
|
||||
:{
|
||||
model: envData.model??MODEL_DEFAULT,
|
||||
model: getModel(envData),
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -198,7 +197,7 @@ const useTranslate = () => {
|
||||
const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef})
|
||||
dispatch(addTaskId(task.id))
|
||||
}
|
||||
}, [dispatch, envData.aiType, envData.apiKey, envData.geminiApiKey, envData.model, envData.prompts, envData.serverUrl, summarizeLanguage.name, title])
|
||||
}, [dispatch, envData, summarizeLanguage.name, title])
|
||||
|
||||
const addAskTask = useCallback(async (segment: Segment, question: string) => {
|
||||
if (segment.text.length >= SUMMARIZE_THRESHOLD) {
|
||||
@@ -228,7 +227,7 @@ const useTranslate = () => {
|
||||
}
|
||||
}
|
||||
:{
|
||||
model: envData.model??MODEL_DEFAULT,
|
||||
model: getModel(envData),
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -251,7 +250,7 @@ const useTranslate = () => {
|
||||
const task = await chrome.runtime.sendMessage({type: 'addTask', taskDef})
|
||||
dispatch(addTaskId(task.id))
|
||||
}
|
||||
}, [dispatch, envData.aiType, envData.apiKey, envData.geminiApiKey, envData.model, envData.prompts, envData.serverUrl, summarizeLanguage.name, title])
|
||||
}, [dispatch, envData, summarizeLanguage.name, title])
|
||||
|
||||
const handleTranslate = useMemoizedFn((task: Task, content: string) => {
|
||||
let map: {[key: string]: string} = {}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import {createSlice, PayloadAction} from '@reduxjs/toolkit'
|
||||
import {find} from 'lodash-es'
|
||||
import {getDevData} from '../util/biz_util'
|
||||
import {SERVER_URL_OPENAI, TOTAL_HEIGHT_DEF} from '../const'
|
||||
import {DEFAULT_SERVER_URL_OPENAI, TOTAL_HEIGHT_DEF} from '../const'
|
||||
|
||||
interface EnvState {
|
||||
envData: EnvData
|
||||
@@ -51,10 +51,10 @@ interface EnvState {
|
||||
|
||||
const initialState: EnvState = {
|
||||
envData: {
|
||||
serverUrl: SERVER_URL_OPENAI,
|
||||
serverUrl: DEFAULT_SERVER_URL_OPENAI,
|
||||
translateEnable: true,
|
||||
summarizeEnable: true,
|
||||
// autoExpand: true,
|
||||
autoExpand: true,
|
||||
theme: 'light',
|
||||
searchEnabled: true,
|
||||
},
|
||||
|
4
src/typings.d.ts
vendored
4
src/typings.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
interface EnvData {
|
||||
// autoExpand?: boolean
|
||||
autoExpand?: boolean
|
||||
flagDot?: boolean
|
||||
|
||||
aiType?: 'openai' | 'gemini'
|
||||
@@ -7,6 +7,8 @@ interface EnvData {
|
||||
apiKey?: string
|
||||
serverUrl?: string
|
||||
model?: string
|
||||
customModel?: string
|
||||
customModelTokens?: number
|
||||
// gemini
|
||||
geminiApiKey?: string
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import devData from '../data/data.json'
|
||||
import {APP_DOM_ID, SUMMARIZE_TYPES} from '../const'
|
||||
import {APP_DOM_ID, CUSTOM_MODEL_TOKENS, MODEL_DEFAULT, MODEL_MAP, SUMMARIZE_TYPES} from '../const'
|
||||
import {isDarkMode} from '@kky002/kky-util'
|
||||
import toast from 'react-hot-toast'
|
||||
import {findIndex} from 'lodash-es'
|
||||
@@ -123,6 +123,22 @@ export const getServerUrl = (serverUrl?: string) => {
|
||||
return serverUrl
|
||||
}
|
||||
|
||||
export const getModel = (envData: EnvData) => {
|
||||
if (envData.model === 'custom') {
|
||||
return envData.customModel
|
||||
} else {
|
||||
return envData.model
|
||||
}
|
||||
}
|
||||
|
||||
export const getModelMaxTokens = (envData: EnvData) => {
|
||||
if (envData.model === 'custom') {
|
||||
return envData.customModelTokens??CUSTOM_MODEL_TOKENS
|
||||
} else {
|
||||
return MODEL_MAP[envData.model??MODEL_DEFAULT]?.tokens??4000
|
||||
}
|
||||
}
|
||||
|
||||
export const setTheme = (theme: EnvData['theme']) => {
|
||||
const appRoot = document.getElementById(APP_DOM_ID)
|
||||
if (appRoot != null) {
|
||||
|
Reference in New Issue
Block a user