Skip to content

Commit

Permalink
split text into caption lines
Browse files Browse the repository at this point in the history
  • Loading branch information
SheepTester committed May 2, 2024
1 parent 4729b43 commit 1b9ad89
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 11 deletions.
26 changes: 15 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { Fragment, useRef, useState } from 'react'
import { Fragment, useMemo, useRef, useState } from 'react'
import { AddIcon } from './components/AddIcon'
import { RemoveIcon } from './components/RemoveIcon'
import { TextArea } from './components/TextArea'
import { PlayIcon } from './components/PlayIcon'
import { PauseIcon } from './components/PauseIcon'

type Annotation =
| { type: 'animation'; value: string }
| { type: 'set-layout'; value: string }
| { type: 'set-background'; value: string }
type Part = (
| { type: 'text'; content: string }
| { type: 'annotation'; annotation: Annotation }
) & { id: number }
import { Part, strategize } from './video-strategy'

export function App () {
const nextId = useRef(0)
Expand Down Expand Up @@ -46,6 +38,11 @@ export function App () {
const [playing, setPlaying] = useState(false)
const [time, setTime] = useState(0)

const previewVideo = useMemo(() => strategize(parts), [parts])
const caption = previewVideo.captions.findLast(
caption => time >= caption.time
) ?? { content: '' }

return (
<div className='editor'>
<div className='output'>
Expand All @@ -54,7 +51,11 @@ export function App () {
<button className='menubar-btn'>File</button>
<button className='menubar-btn'>Edit</button>
</div>
<div className='preview'></div>
<div className='preview'>
<div className='caption'>
<span>{caption.content}</span>
</div>
</div>
<div className='controls'>
<button className='play-btn' onClick={() => setPlaying(!playing)}>
{playing ? <PauseIcon /> : <PlayIcon />}
Expand All @@ -63,6 +64,9 @@ export function App () {
type='range'
value={time}
onChange={e => setTime(e.currentTarget.valueAsNumber)}
step='any'
min={0}
max={previewVideo.length}
className='scrubber'
/>
</div>
Expand Down
17 changes: 17 additions & 0 deletions src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,26 @@ button:not(:disabled) {
.preview {
aspect-ratio: 16 / 9;
border-radius: 10px;
position: relative;
/* TEMP */
background-image: linear-gradient(-50deg, #f97316, #ec4899);
}
.caption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
margin: 10px;
text-align: center;
font-size: 12px;

& span {
background-color: rgba(0, 0, 0, 0.5);
color: white;
padding: 5px 10px;
border-radius: 5px;
}
}
.controls {
display: flex;
align-items: center;
Expand Down
69 changes: 69 additions & 0 deletions src/video-strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export type Annotation =
| { type: 'animation'; value: string }
| { type: 'set-layout'; value: string }
| { type: 'set-background'; value: string }
export type PartBase =
| { type: 'text'; content: string }
| { type: 'annotation'; annotation: Annotation }
export type Part = PartBase & { id: number }

export type Caption = {
time: number
content: string
}
export type Action = Annotation & { time: number }
export type Stategy = {
captions: Caption[]
actions: Action[]
length: number
}
const MAX_CAPTION_LENGTH = 50
// Speaking 150~160 wpm, 5~6.5 chars/word, so 160 * 5 chars/min, or `minute /
// (160 * 5)` = 75 milliseconds/char
const EST_TIME_PER_CHAR = 75
export function strategize (parts: PartBase[]): Stategy {
const captions: Caption[] = []
const actions: Action[] = []

let time = 0
for (const part of parts) {
if (part.type === 'text') {
const lines = part.content.split(/\s*\n\s*/)
for (const line of lines) {
let caption = ''
let lastIndex = 0
for (const { index } of line.matchAll(/\s+/g)) {
// `index` is the index of the first whitespace character
const appendum = line.slice(lastIndex, index)
if (caption.length + appendum.length > MAX_CAPTION_LENGTH) {
const content = caption.trim()
captions.push({ content, time })
time += content.replace(/\s/g, '').length * EST_TIME_PER_CHAR
caption = appendum
} else {
caption += appendum
}
lastIndex = index
}
const rest = line.slice(lastIndex)
if (caption.length + rest.length > MAX_CAPTION_LENGTH) {
const content = caption.trim()
captions.push({ content, time })
time += content.replace(/\s/g, '').length * EST_TIME_PER_CHAR
caption = rest
} else {
caption += rest
}
if (caption.length > 0) {
const content = caption.trim()
captions.push({ content, time })
time += content.replace(/\s/g, '').length * EST_TIME_PER_CHAR
}
}
} else {
actions.push({ ...part.annotation, time })
}
}

return { captions, actions, length: time }
}

0 comments on commit 1b9ad89

Please sign in to comment.