Spaces:
Running
Running
| import { Group, Meta, Transcript, TweetExtra } from "common-utils"; | |
| import { AbsoluteFill } from "remotion"; | |
| import { RenderUtils } from "../RenderUtils"; | |
| function formatTweetDate(date: Date) { | |
| const hours = date.getHours(); | |
| const minutes = date.getMinutes(); | |
| const ampm = hours >= 12 ? "PM" : "AM"; | |
| const hour12 = hours % 12 === 0 ? 12 : hours % 12; | |
| const minStr = minutes.toString().padStart(2, "0"); | |
| const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | |
| const month = months[date.getMonth()]; | |
| const day = date.getDate(); | |
| const year = date.getFullYear(); | |
| return `${hour12}:${minStr} ${ampm} · ${month} ${day}, ${year}`; | |
| } | |
| export default function TweetPoster({ | |
| transcript, | |
| meta, | |
| }: { | |
| transcript: Transcript; | |
| meta: Meta; | |
| }) { | |
| const textParts = transcript.audioCaption?.words; | |
| if (!textParts) { | |
| return ( | |
| <AbsoluteFill color="#ffffff"> | |
| <h1> | |
| <div>Empty tweet text!</div> | |
| </h1> | |
| </AbsoluteFill> | |
| ); | |
| } | |
| const { width, height } = meta.resolution || { width: 1080, height: 1920 }; | |
| // Scale solely from WIDTH (as requested) | |
| const W = Math.max(320, width); // guard for tiny canvases | |
| // Outer padding around the card (kept modest so card fills width but not flush) | |
| const outerPad = Math.max(Math.round(W * 0.04), 16); // 4% of width, min 16px | |
| // Card internal padding | |
| const boxPad = Math.max(Math.round(W * 0.03), 14); // 3% of width, min 14px | |
| // Avatar + typography strictly from width | |
| const avatarSize = Math.max(Math.round(W * 0.08), 56); // 8% of width | |
| const fontNamePx = Math.round(W * 0.035); // display name | |
| const fontUserPx = Math.round(W * 0.028); // @username | |
| const fontMainPx = Math.round(W * 0.042); // tweet text | |
| const fontDatePx = Math.round(W * 0.026); // footer date | |
| // Reasonable clamps to avoid extremes on very large/small canvases | |
| const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v)); | |
| const fontName = `${clamp(fontNamePx, 16, 40)}px`; | |
| const fontUsername = `${clamp(fontUserPx, 14, 32)}px`; | |
| const fontMain = `${clamp(fontMainPx, 18, 48)}px`; | |
| const fontDate = `${clamp(fontDatePx, 12, 28)}px`; | |
| let bgColor = (transcript.extras as TweetExtra)?.textBgColor || "#ffffff"; | |
| const fullName = textParts[0]?.word; | |
| const fullNameStyle = textParts[0]?.textStyle; | |
| const username = textParts[1]?.word; | |
| const usernameStyle = textParts[1]?.textStyle; | |
| const tweetLine1 = textParts[2]?.word; | |
| const tweetLine1Style = textParts[2]?.textStyle; | |
| const tweetLine2 = textParts[3]?.word; | |
| const tweetLine2Style = textParts[3]?.textStyle; | |
| const avatar = RenderUtils.tryStaticFile(transcript.mediaAbsPaths?.[0]?.path); | |
| return ( | |
| <AbsoluteFill | |
| style={{ | |
| background: bgColor, | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| width: "100%", | |
| height: "100%", | |
| boxSizing: "border-box", | |
| padding: 0, | |
| }} | |
| > | |
| {/* Outer pad keeps some breathing room; box fills remaining width */} | |
| <div | |
| style={{ | |
| width: "100%", | |
| height: "100%", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "center", | |
| boxSizing: "border-box", | |
| padding: outerPad, | |
| }} | |
| > | |
| {/* Tweet box now fully fits parent width with the outerPad margin */} | |
| <div | |
| style={{ | |
| width: "100%", | |
| // height is auto so small/large content won't crop; | |
| // allow it to grow and just keep nice rounding/shadow | |
| background: "#fff", | |
| border: "1px solid #e1e8ed", | |
| borderRadius: Math.max(Math.round(W * 0.02), 16), | |
| boxShadow: `0 ${Math.round(W * 0.04)}px ${Math.round( | |
| W * 0.08 | |
| )}px rgba(0,0,0,0.10)`, | |
| padding: `${boxPad}px ${Math.round(boxPad * 1.2)}px`, | |
| fontFamily: "Segoe UI, Arial, sans-serif", | |
| boxSizing: "border-box", | |
| display: "flex", | |
| flexDirection: "column", | |
| justifyContent: "flex-start", | |
| // Prevent cropping: let content define height; wrap long words | |
| overflow: "visible", | |
| wordBreak: "break-word", | |
| }} | |
| > | |
| {/* Header */} | |
| <div | |
| style={{ | |
| display: "flex", | |
| alignItems: "center", | |
| marginBottom: boxPad, | |
| minWidth: 0, | |
| }} | |
| > | |
| <img | |
| src={avatar} | |
| alt="avatar" | |
| style={{ | |
| width: avatarSize, | |
| height: avatarSize, | |
| minWidth: 40, | |
| minHeight: 40, | |
| borderRadius: "50%", | |
| marginRight: boxPad, | |
| border: "2px solid #e1e8ed", | |
| objectFit: "cover", | |
| flexShrink: 0, | |
| }} | |
| /> | |
| <div style={{ minWidth: 0 }}> | |
| <div | |
| style={{ | |
| fontWeight: 700, | |
| fontSize: fontName, | |
| lineHeight: 1.15, | |
| whiteSpace: "pre-wrap", | |
| ...fullNameStyle, | |
| }} | |
| > | |
| {fullName} | |
| </div> | |
| <div | |
| style={{ | |
| color: "#657786", | |
| fontSize: fontUsername, | |
| lineHeight: 1.15, | |
| whiteSpace: "pre-wrap", | |
| ...usernameStyle, | |
| }} | |
| > | |
| {username} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Tweet text */} | |
| <div | |
| style={{ | |
| fontSize: fontMain, | |
| lineHeight: 1.45, | |
| marginBottom: boxPad, | |
| whiteSpace: "pre-wrap", | |
| }} | |
| > | |
| <span style={tweetLine1Style}>{tweetLine1}</span> | |
| {tweetLine2 && ( | |
| <> | |
| {"\n"} | |
| <span style={tweetLine2Style}>{tweetLine2}</span> | |
| </> | |
| )} | |
| </div> | |
| {/* Footer */} | |
| <div | |
| style={{ | |
| color: "#657786", | |
| fontSize: fontDate, | |
| marginTop: Math.round(boxPad * 0.5), | |
| display: "flex", | |
| gap: Math.round(W * 0.01), | |
| alignItems: "baseline", | |
| flexWrap: "wrap", | |
| }} | |
| > | |
| <span>{formatTweetDate(new Date())}</span> | |
| <span>·</span> | |
| <span style={{ color: "#1da1f2" }}>Follow</span> | |
| </div> | |
| </div> | |
| </div> | |
| </AbsoluteFill> | |
| ); | |
| } | |