This commit is contained in:
IndieKKY
2024-10-05 20:03:02 +08:00
parent b283695b02
commit d52231227e
33 changed files with 44 additions and 51 deletions

312
src/utils/biz_util.ts Normal file
View 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
View 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
View 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
View 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()
}