You've already forked bilibili-subtitle
fix
This commit is contained in:
312
src/utils/biz_util.ts
Normal file
312
src/utils/biz_util.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import {APP_DOM_ID, CUSTOM_MODEL_TOKENS, MODEL_DEFAULT, MODEL_MAP, SUMMARIZE_TYPES} from '../consts/const'
|
||||
import {isDarkMode} from '@kky002/kky-util'
|
||||
import toast from 'react-hot-toast'
|
||||
import {findIndex} from 'lodash-es'
|
||||
|
||||
export const debug = (...args: any[]) => {
|
||||
console.debug('[APP]', ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取译文
|
||||
*/
|
||||
export const getTransText = (transResult: TransResult, hideOnDisableAutoTranslate: boolean | undefined, autoTranslate: boolean | undefined) => {
|
||||
if (transResult && (!transResult.code || transResult.code === '200') && (autoTranslate === true || !hideOnDisableAutoTranslate) && transResult.data) {
|
||||
return transResult.data
|
||||
}
|
||||
}
|
||||
|
||||
export const getDisplay = (transDisplay_: EnvData['transDisplay'], content: string, transText: string | undefined) => {
|
||||
const transDisplay = transDisplay_ ?? 'originPrimary'
|
||||
let main, sub
|
||||
// main
|
||||
if (transText && (transDisplay === 'targetPrimary' || transDisplay === 'target')) {
|
||||
main = transText
|
||||
} else {
|
||||
main = content
|
||||
}
|
||||
// sub
|
||||
switch (transDisplay) {
|
||||
case 'originPrimary':
|
||||
sub = transText
|
||||
break
|
||||
case 'targetPrimary':
|
||||
if (transText) {
|
||||
sub = content
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
// return
|
||||
return {
|
||||
main,
|
||||
sub,
|
||||
}
|
||||
}
|
||||
|
||||
export const getWholeText = (items: string[]) => {
|
||||
return items.join(',').replaceAll('\n', ' ')
|
||||
}
|
||||
|
||||
export const getLastTime = (seconds: number) => {
|
||||
if (seconds > 60 * 60) {
|
||||
return `${Math.floor(seconds / 60 / 60)}小时`
|
||||
}
|
||||
if (seconds > 60) {
|
||||
return `${Math.floor(seconds / 60)}分钟`
|
||||
}
|
||||
return `${Math.floor(seconds)}秒`
|
||||
}
|
||||
|
||||
/**
|
||||
* 00:00:00
|
||||
*/
|
||||
export const getTimeDisplay = (seconds: number) => {
|
||||
const h = Math.floor(seconds / 60 / 60)
|
||||
const m = Math.floor(seconds / 60 % 60)
|
||||
const s = Math.floor(seconds % 60)
|
||||
return `${h < 10 ? '0' : ''}${h}:${m < 10 ? '0' : ''}${m}:${s < 10 ? '0' : ''}${s}`
|
||||
}
|
||||
|
||||
export const isSummaryEmpty = (summary: Summary) => {
|
||||
if (summary.type === 'overview') {
|
||||
const content: OverviewItem[] = summary.content??[]
|
||||
return content.length === 0
|
||||
} else if (summary.type === 'keypoint') {
|
||||
const content: string[] = summary.content??[]
|
||||
return content.length === 0
|
||||
} else if (summary.type === 'brief') {
|
||||
const content: string[] = summary.content??''
|
||||
return content.length === 0
|
||||
} else if (summary.type === 'question') {
|
||||
const content: any[] = summary.content??[]
|
||||
return content.length === 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const getSummaryStr = (summary: Summary) => {
|
||||
let s = ''
|
||||
if (summary.type === 'overview') {
|
||||
const content: OverviewItem[] = summary.content ?? []
|
||||
for (const overviewItem of content) {
|
||||
s += (overviewItem.emoji ?? '') + overviewItem.time + ' ' + overviewItem.key + '\n'
|
||||
}
|
||||
} else if (summary.type === 'keypoint') {
|
||||
const content: string[] = summary.content ?? []
|
||||
for (const keypoint of content) {
|
||||
s += '- ' + keypoint + '\n'
|
||||
}
|
||||
} else if (summary.type === 'brief') {
|
||||
const content: { summary: string } = summary.content ?? {
|
||||
summary: ''
|
||||
}
|
||||
s += content.summary + '\n'
|
||||
} else if (summary.type === 'question') {
|
||||
const content: Array<{ q: string, a: string }> = summary.content ?? []
|
||||
s += content.map(item => {
|
||||
return item.q + '\n' + item.a + '\n'
|
||||
}).join('\n')
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
export const getServerUrl = (serverUrl?: string) => {
|
||||
if (!serverUrl) {
|
||||
return 'https://api.openai.com'
|
||||
}
|
||||
if (serverUrl.endsWith('/')) {
|
||||
serverUrl = serverUrl.slice(0, -1)
|
||||
}
|
||||
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) {
|
||||
// system
|
||||
theme = theme ?? 'system'
|
||||
if (!theme || theme === 'system') {
|
||||
theme = isDarkMode() ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
appRoot.setAttribute('data-theme', theme)
|
||||
if (theme === 'dark') {
|
||||
appRoot.classList.add('dark')
|
||||
appRoot.classList.remove('light')
|
||||
} else {
|
||||
appRoot.classList.add('light')
|
||||
appRoot.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getSummarize = (title: string | undefined, segments: Segment[] | undefined, type: SummaryType): [boolean, string] => {
|
||||
if (segments == null) {
|
||||
return [false, '']
|
||||
}
|
||||
|
||||
let content = `${SUMMARIZE_TYPES[type]?.downloadName ?? ''}\n\n`
|
||||
let success = false
|
||||
for (const segment of segments) {
|
||||
const summary = segment.summaries[type]
|
||||
if (summary && !isSummaryEmpty(summary)) {
|
||||
success = true
|
||||
content += getSummaryStr(summary)
|
||||
} else {
|
||||
if (segment.items.length > 0) {
|
||||
content += `${getTimeDisplay(segment.items[0].from)} `
|
||||
}
|
||||
content += '未总结\n'
|
||||
}
|
||||
}
|
||||
|
||||
content += '\n--- 哔哩哔哩字幕列表扩展'
|
||||
|
||||
if (!success) {
|
||||
toast.error('未找到总结')
|
||||
}
|
||||
|
||||
return [success, content]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param time '03:10'
|
||||
*/
|
||||
export const parseStrTimeToSeconds = (time: string): number => {
|
||||
const parts = time.split(':')
|
||||
return parseInt(parts[0]) * 60 + parseInt(parts[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param time '00:04:11,599' or '00:04:11.599' or '04:11,599' or '04:11.599'
|
||||
* @return seconds, 4.599
|
||||
*/
|
||||
export const parseTime = (time: string): number => {
|
||||
const separator = time.includes(',') ? ',' : '.'
|
||||
const parts = time.split(':')
|
||||
const ms = parts[parts.length-1].split(separator)
|
||||
if (parts.length === 3) {
|
||||
return parseInt(parts[0]) * 60 * 60 + parseInt(parts[1]) * 60 + parseInt(ms[0]) + parseInt(ms[1]) / 1000
|
||||
} else {
|
||||
return parseInt(parts[0]) * 60 + parseInt(ms[0]) + parseInt(ms[1]) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
export const parseTranscript = (filename: string, text: string | ArrayBuffer): Transcript => {
|
||||
const items: TranscriptItem[] = []
|
||||
// convert /r/n to /n
|
||||
text = (text as string).trim().replace(/\r\n/g, '\n')
|
||||
// .srt:
|
||||
if (filename.toLowerCase().endsWith('.srt')) {
|
||||
const lines = text.split('\n\n')
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const linesInner = line.trim().split('\n')
|
||||
if (linesInner.length >= 3) {
|
||||
const time = linesInner[1].split(' --> ')
|
||||
const from = parseTime(time[0])
|
||||
const to = parseTime(time[1])
|
||||
const content = linesInner.slice(2).join('\n')
|
||||
items.push({
|
||||
from,
|
||||
to,
|
||||
content,
|
||||
idx: items.length,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('parse error', line)
|
||||
}
|
||||
}
|
||||
}
|
||||
// .vtt:
|
||||
if (filename.toLowerCase().endsWith('.vtt')) {
|
||||
const lines = text.split('\n\n')
|
||||
for (const line of lines) {
|
||||
const lines = line.split('\n')
|
||||
const timeIdx = findIndex(lines, (line) => line.includes('-->'))
|
||||
if (timeIdx >= 0) {
|
||||
const time = lines[timeIdx].split(' --> ')
|
||||
const from = parseTime(time[0])
|
||||
const to = parseTime(time[1])
|
||||
const content = lines.slice(timeIdx + 1).join('\n')
|
||||
items.push({
|
||||
from,
|
||||
to,
|
||||
content,
|
||||
idx: items.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// return
|
||||
return {
|
||||
body: items,
|
||||
}
|
||||
}
|
||||
|
||||
export const extractJsonObject = (content: string) => {
|
||||
// get content between ``` and ```
|
||||
const start = content.indexOf('```')
|
||||
const end = content.lastIndexOf('```')
|
||||
if (start >= 0 && end >= 0) {
|
||||
if (start === end) { // 异常情况
|
||||
if (content.startsWith('```')) {
|
||||
content = content.slice(3)
|
||||
} else {
|
||||
content = content.slice(0, -3)
|
||||
}
|
||||
} else {
|
||||
content = content.slice(start + 3, end)
|
||||
}
|
||||
}
|
||||
// get content between { and }
|
||||
const start2 = content.indexOf('{')
|
||||
const end2 = content.lastIndexOf('}')
|
||||
if (start2 >= 0 && end2 >= 0) {
|
||||
content = content.slice(start2, end2 + 1)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
export const extractJsonArray = (content: string) => {
|
||||
// get content between ``` and ```
|
||||
const start = content.indexOf('```')
|
||||
const end = content.lastIndexOf('```')
|
||||
if (start >= 0 && end >= 0) {
|
||||
if (start === end) { // 异常情况
|
||||
if (content.startsWith('```')) {
|
||||
content = content.slice(3)
|
||||
} else {
|
||||
content = content.slice(0, -3)
|
||||
}
|
||||
} else {
|
||||
content = content.slice(start + 3, end)
|
||||
}
|
||||
}
|
||||
// get content between [ and ]
|
||||
const start3 = content.indexOf('[')
|
||||
const end3 = content.lastIndexOf(']')
|
||||
if (start3 >= 0 && end3 >= 0) {
|
||||
content = content.slice(start3, end3 + 1)
|
||||
}
|
||||
return content
|
||||
}
|
125
src/utils/pinyin_util.ts
Normal file
125
src/utils/pinyin_util.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import pinyin from 'tiny-pinyin'
|
||||
import {uniq} from 'lodash-es'
|
||||
|
||||
/**
|
||||
* pinyin的返回结果
|
||||
*/
|
||||
interface Ret {
|
||||
type: 1 | 2 | 3
|
||||
source: string
|
||||
target: string
|
||||
}
|
||||
|
||||
interface Phase {
|
||||
pinyin: boolean
|
||||
list: Ret[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Phase列表(中英文分离列表)
|
||||
*/
|
||||
export const getPhases = (str: string) => {
|
||||
const rets = pinyin.parse(str)
|
||||
|
||||
const phases: Phase[] = []
|
||||
let curPinyin_ = false
|
||||
let curPhase_: Ret[] = []
|
||||
const addCurrentPhase = () => {
|
||||
if (curPhase_.length > 0) {
|
||||
phases.push({
|
||||
pinyin: curPinyin_,
|
||||
list: curPhase_,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历rets
|
||||
for (const ret of rets) {
|
||||
const newPinyin = ret.type === 2
|
||||
// 如果跟旧的pinyin类型不同,先保存旧的
|
||||
if (newPinyin !== curPinyin_) {
|
||||
addCurrentPhase()
|
||||
// 重置
|
||||
curPinyin_ = newPinyin
|
||||
curPhase_ = []
|
||||
}
|
||||
// 添加新的
|
||||
curPhase_.push(ret)
|
||||
}
|
||||
// 最后一个
|
||||
addCurrentPhase()
|
||||
|
||||
return phases
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原子字符列表,如 tool tab 汉 字
|
||||
*/
|
||||
export const getAtoms = (str: string) => {
|
||||
const phases = getPhases(str)
|
||||
|
||||
const atoms = []
|
||||
for (const phase of phases) {
|
||||
if (phase.pinyin) { // all words
|
||||
atoms.push(...phase.list.map(e => e.source).filter(e => e))
|
||||
} else { // split
|
||||
atoms.push(...(phase.list.map((e: any) => e.source).join('').match(/\w+/g)??[]).filter((e: string) => e))
|
||||
}
|
||||
}
|
||||
|
||||
return atoms
|
||||
}
|
||||
|
||||
const fixStrs = (atoms: string[]) => {
|
||||
// 小写
|
||||
atoms = atoms.map(e => e.toLowerCase())
|
||||
|
||||
// 去重
|
||||
atoms = uniq(atoms)
|
||||
|
||||
// 返回
|
||||
return atoms
|
||||
}
|
||||
|
||||
export const getWords = (str: string) => {
|
||||
// 获取全部原子字符
|
||||
const atoms = getAtoms(str)
|
||||
// fix
|
||||
return fixStrs(atoms)
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的世界Minecraft => ['wodeshijie', 'deshijie', 'shijie', 'jie'] + ['wdsj', 'dsj', 'sj', 'j']
|
||||
*
|
||||
* 1. only handle pinyin, other is ignored
|
||||
*/
|
||||
export const getWordsPinyin = (str: string) => {
|
||||
let result: string[] = []
|
||||
|
||||
for (const phase of getPhases(str)) {
|
||||
// only handle pinyin
|
||||
if (phase.pinyin) { // 我的世界
|
||||
// 获取全部原子字符
|
||||
// 我的世界 => [我, 的, 世, 界]
|
||||
const atoms: string[] = []
|
||||
atoms.push(...phase.list.map(e => e.source).filter(e => e))
|
||||
// 获取全部子串
|
||||
// [我, 的, 世, 界] => [我的世界, 的世界, 世界, 界]
|
||||
const allSubStr = []
|
||||
for (let i = 0; i < atoms.length; i++) {
|
||||
allSubStr.push(atoms.slice(i).join(''))
|
||||
}
|
||||
// pinyin version
|
||||
const pinyinList = allSubStr.map((e: string) => pinyin.convertToPinyin(e))
|
||||
result.push(...pinyinList)
|
||||
// pinyin first version
|
||||
const pinyinFirstList = allSubStr.map((e: string) => pinyin.parse(e).map((e: any) => e.type === 2?e.target[0]:null).filter(e => !!e).join(''))
|
||||
result.push(...pinyinFirstList)
|
||||
}
|
||||
}
|
||||
|
||||
// fix
|
||||
result = fixStrs(result)
|
||||
|
||||
return result
|
||||
}
|
65
src/utils/search.ts
Normal file
65
src/utils/search.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as JsSearch from 'js-search'
|
||||
import {uniq} from 'lodash-es'
|
||||
import {getWords, getWordsPinyin} from './pinyin_util'
|
||||
|
||||
const tokenize = (maxLength: number, content: string, options?: SearchOptions) => {
|
||||
const result: string[] = []
|
||||
|
||||
// 最大长度
|
||||
if (content.length > maxLength) {
|
||||
content = content.substring(0, maxLength)
|
||||
}
|
||||
result.push(...getWords(content))
|
||||
// check cn
|
||||
if (options?.cnSearchEnabled) {
|
||||
result.push(...getWordsPinyin(content))
|
||||
}
|
||||
|
||||
// console.debug('[Search] tokenize:', str, '=>', result)
|
||||
|
||||
return uniq(result)
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
cnSearchEnabled?: boolean
|
||||
}
|
||||
|
||||
export const Search = (uidFieldName: string, index: string, maxLength: number, options?: SearchOptions) => {
|
||||
let searchRef: JsSearch.Search | undefined// 搜索器
|
||||
|
||||
/**
|
||||
* 重置索引
|
||||
*/
|
||||
const reset = (documents?: Object[]) => {
|
||||
// 搜索器
|
||||
searchRef = new JsSearch.Search(uidFieldName)
|
||||
searchRef.tokenizer = {
|
||||
tokenize: (str) => {
|
||||
return tokenize(maxLength, str, options)
|
||||
}
|
||||
}
|
||||
searchRef.addIndex(index)
|
||||
|
||||
// 检测添加文档
|
||||
if (documents != null) {
|
||||
searchRef.addDocuments(documents)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加文档
|
||||
*/
|
||||
const add = (document: Object) => {
|
||||
searchRef?.addDocument(document)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @return 未去重
|
||||
*/
|
||||
const search = (text: string) => {
|
||||
return searchRef?.search(text.toLowerCase())
|
||||
}
|
||||
|
||||
return {reset, add, search}
|
||||
}
|
48
src/utils/util.ts
Normal file
48
src/utils/util.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {SyntheticEvent} from 'react'
|
||||
|
||||
export const isEdgeBrowser = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('edg/') && !userAgent.includes('edge/');
|
||||
}
|
||||
|
||||
export const formatTime = (time: number) => {
|
||||
if (!time) return '00:00'
|
||||
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param time 2.82
|
||||
*/
|
||||
export const formatSrtTime = (time: number) => {
|
||||
if (!time) return '00:00:00,000'
|
||||
|
||||
const hours = Math.floor(time / 60 / 60)
|
||||
const minutes = Math.floor(time / 60 % 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
const ms = Math.floor((time % 1) * 1000)
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @param time 2.82
|
||||
*/
|
||||
export const formatVttTime = (time: number) => {
|
||||
if (!time) return '00:00:00.000'
|
||||
|
||||
const hours = Math.floor(time / 60 / 60)
|
||||
const minutes = Math.floor(time / 60 % 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
const ms = Math.floor((time % 1) * 1000)
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`
|
||||
}
|
||||
|
||||
export const preventFunc = (e: SyntheticEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
export const stopPopFunc = (e: SyntheticEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
Reference in New Issue
Block a user