mirror of
https://github.com/calesthio/OpenMontage.git
synced 2026-08-25 09:34:21 +08:00
feat(remotion-composer): CJK-aware captions and caption styling props
CaptionOverlay assumes space-delimited text: it hardcodes a " " between words and lets lines wrap anywhere inside a word. For CJK captions this produces spurious inter-word spaces (Japanese does not use them) and mid-word line breaks, including punctuation stranded at line starts. Changes: - CaptionOverlay: new wordSeparator prop (default " " keeps current behavior; CJK callers pass ""). Each word renders as an unbreakable inline block so lines wrap only at word boundaries - visually unchanged for space-delimited text, fixes mid-word breaks for CJK. - WordCaption: optional pageBreakAfter flag; buildPages flushes the page early when set, so pages can align with sentence/scene boundaries instead of splitting a clause across pages. - TalkingHead: exposes captionColor / captionBackgroundColor (previously hardcoded inline), captionFontFamily, and captionWordSeparator, all defaulting to the current values. Tested by rendering TalkingHead stills with Japanese captions before and after (screenshots in PR), and with default props to confirm the space-delimited rendering is unchanged. Used in production for Japanese vertical ad videos.
This commit is contained in:
@@ -302,6 +302,11 @@ export interface TalkingHeadProps {
|
||||
wordsPerPage?: number;
|
||||
fontSize?: number;
|
||||
highlightColor?: string;
|
||||
captionColor?: string;
|
||||
captionBackgroundColor?: string;
|
||||
captionFontFamily?: string;
|
||||
// Pass "" for CJK captions (no inter-word spacing); defaults to " ".
|
||||
captionWordSeparator?: string;
|
||||
}
|
||||
|
||||
export const TalkingHead: React.FC<TalkingHeadProps> = ({
|
||||
@@ -311,6 +316,10 @@ export const TalkingHead: React.FC<TalkingHeadProps> = ({
|
||||
wordsPerPage = 4,
|
||||
fontSize = 52,
|
||||
highlightColor = "#22D3EE",
|
||||
captionColor = "#FFFFFF",
|
||||
captionBackgroundColor = "rgba(0, 0, 0, 0.65)",
|
||||
captionFontFamily,
|
||||
captionWordSeparator,
|
||||
}) => {
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
@@ -345,8 +354,10 @@ export const TalkingHead: React.FC<TalkingHeadProps> = ({
|
||||
wordsPerPage={wordsPerPage}
|
||||
fontSize={fontSize}
|
||||
highlightColor={highlightColor}
|
||||
backgroundColor="rgba(0, 0, 0, 0.65)"
|
||||
color="#FFFFFF"
|
||||
backgroundColor={captionBackgroundColor}
|
||||
color={captionColor}
|
||||
{...(captionFontFamily ? { fontFamily: captionFontFamily } : {})}
|
||||
{...(captionWordSeparator !== undefined ? { wordSeparator: captionWordSeparator } : {})}
|
||||
/>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,9 @@ export interface WordCaption {
|
||||
word: string;
|
||||
startMs: number;
|
||||
endMs: number;
|
||||
// Force a page break after this word (e.g. sentence or scene boundaries).
|
||||
// Useful for CJK captions where pages should align with clause boundaries.
|
||||
pageBreakAfter?: boolean;
|
||||
}
|
||||
|
||||
type CaptionOverlayProps = {
|
||||
@@ -23,6 +26,9 @@ type CaptionOverlayProps = {
|
||||
highlightColor?: string;
|
||||
backgroundColor?: string;
|
||||
fontFamily?: string;
|
||||
// Separator rendered between words. Space-delimited languages want the
|
||||
// default " "; CJK languages (no inter-word spacing) should pass "".
|
||||
wordSeparator?: string;
|
||||
};
|
||||
|
||||
interface CaptionPage {
|
||||
@@ -33,15 +39,21 @@ interface CaptionPage {
|
||||
|
||||
function buildPages(words: WordCaption[], wordsPerPage: number): CaptionPage[] {
|
||||
const pages: CaptionPage[] = [];
|
||||
for (let i = 0; i < words.length; i += wordsPerPage) {
|
||||
const pageWords = words.slice(i, i + wordsPerPage);
|
||||
if (pageWords.length === 0) continue;
|
||||
let pageWords: WordCaption[] = [];
|
||||
const flush = () => {
|
||||
if (pageWords.length === 0) return;
|
||||
pages.push({
|
||||
words: pageWords,
|
||||
startMs: pageWords[0].startMs,
|
||||
endMs: pageWords[pageWords.length - 1].endMs,
|
||||
});
|
||||
pageWords = [];
|
||||
};
|
||||
for (const w of words) {
|
||||
pageWords.push(w);
|
||||
if (pageWords.length >= wordsPerPage || w.pageBreakAfter) flush();
|
||||
}
|
||||
flush();
|
||||
return pages;
|
||||
}
|
||||
|
||||
@@ -52,7 +64,8 @@ const PageRenderer: React.FC<{
|
||||
highlightColor: string;
|
||||
backgroundColor: string;
|
||||
fontFamily: string;
|
||||
}> = ({ page, fontSize, color, highlightColor, backgroundColor, fontFamily }) => {
|
||||
wordSeparator: string;
|
||||
}> = ({ page, fontSize, color, highlightColor, backgroundColor, fontFamily, wordSeparator }) => {
|
||||
const frame = useCurrentFrame();
|
||||
const { fps } = useVideoConfig();
|
||||
|
||||
@@ -100,6 +113,11 @@ const PageRenderer: React.FC<{
|
||||
<span
|
||||
key={`${w.startMs}-${i}`}
|
||||
style={{
|
||||
// Keep each word unbroken so lines wrap only at word
|
||||
// boundaries. For space-delimited text this matches the
|
||||
// previous behavior; for CJK it prevents mid-word breaks.
|
||||
display: "inline-block",
|
||||
whiteSpace: "nowrap",
|
||||
color: isActive ? highlightColor : isPast ? color : `${color}99`,
|
||||
transition: "none", // CSS transitions forbidden in Remotion
|
||||
textShadow: isActive
|
||||
@@ -107,7 +125,7 @@ const PageRenderer: React.FC<{
|
||||
: "0 2px 4px rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
{w.word}{i < page.words.length - 1 ? " " : ""}
|
||||
{w.word}{i < page.words.length - 1 ? wordSeparator : ""}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
@@ -125,6 +143,7 @@ export const CaptionOverlay: React.FC<CaptionOverlayProps> = ({
|
||||
highlightColor = "#22D3EE",
|
||||
backgroundColor = "rgba(15, 23, 42, 0.75)",
|
||||
fontFamily = "Space Grotesk, Inter, system-ui, sans-serif",
|
||||
wordSeparator = " ",
|
||||
}) => {
|
||||
const { fps } = useVideoConfig();
|
||||
const pages = buildPages(words, wordsPerPage);
|
||||
@@ -148,6 +167,7 @@ export const CaptionOverlay: React.FC<CaptionOverlayProps> = ({
|
||||
highlightColor={highlightColor}
|
||||
backgroundColor={backgroundColor}
|
||||
fontFamily={fontFamily}
|
||||
wordSeparator={wordSeparator}
|
||||
/>
|
||||
</Sequence>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user