-
-
Notifications
You must be signed in to change notification settings - Fork 8
The logo goes on every clip, the outro plays whole, and a guest has a name #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -575,6 +575,15 @@ def cmd_process(args): | |
| config["format"] = args.format | ||
| if getattr(args, "profile", None): | ||
| config["profile"] = args.profile | ||
| if getattr(args, "name_card", None): | ||
| config["name_card"] = { | ||
| "title": args.name_card, | ||
| "subtitle": getattr(args, "name_card_sub", None), | ||
| "seconds": getattr(args, "name_card_seconds", None), | ||
| "accent": getattr(args, "name_card_accent", None), | ||
| } | ||
| if getattr(args, "bookend_fade", None) is not None: | ||
| config["bookend_fade"] = args.bookend_fade | ||
| if getattr(args, "thumbnails", None) is not None: | ||
| config["generate_thumbnails"] = args.thumbnails | ||
| if args.top: | ||
|
|
@@ -1055,6 +1064,8 @@ def _transcribe_progress(pct, msg): | |
| logo_path=config.get("logo_path") or None, | ||
| outro_path=config.get("outro_path") or None, | ||
| intro_path=config.get("intro_path") or None, | ||
| name_card=config.get("name_card"), | ||
| bookend_fade=config.get("bookend_fade", 0.0), | ||
|
Comment on lines
+1067
to
+1068
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Forward overlay settings during re-renders. The initial render receives Pass the same two configuration values to every re-render 🤖 Prompt for AI Agents |
||
| keep_segments=clip.get("segments"), | ||
| face_map=face_map, | ||
| allow_ass_fallback=config.get("allow_ass_fallback", False), | ||
|
|
@@ -3989,6 +4000,16 @@ def main(): | |
| proc.add_argument("--profile", choices=["podcast", "party", "action"], help="Detection profile: podcast (transcript-first, default), party/action (laughter/energy highlights)") | ||
| proc.add_argument("--logo", help="Logo image (asset name or path)") | ||
| proc.add_argument("--outro", help="Outro video (asset name or path)") | ||
| proc.add_argument("--name-card", dest="name_card", | ||
| help="Lower third naming the speaker, shown for the first seconds") | ||
| proc.add_argument("--name-card-sub", dest="name_card_sub", | ||
| help="Second line of the lower third") | ||
| proc.add_argument("--name-card-seconds", dest="name_card_seconds", type=float, | ||
| help="How long the lower third holds (default 3)") | ||
| proc.add_argument("--name-card-accent", dest="name_card_accent", | ||
| help="Underline colour on the lower third") | ||
| proc.add_argument("--bookend-fade", dest="bookend_fade", type=float, default=0.0, | ||
| help="Seconds of crossfade into an intro or outro (default 0, a cut)") | ||
| proc.add_argument("--no-outro", action="store_true", help="Do not append an outro (default for highlight profiles)") | ||
| proc.add_argument("--intro", help="Intro video (asset name or path)") | ||
| proc.add_argument("--time-adjust", type=float, help="Timestamp offset in seconds") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -465,6 +465,7 @@ def _render_with_remotion( | |
| output_path: str, | ||
| time_offset: float = 0.0, | ||
| logo_path: Optional[str] = None, | ||
| name_card: Optional[dict] = None, | ||
| keep_caption_overlay: bool = False, | ||
| ) -> tuple[bool, Optional[str]]: | ||
| """ | ||
|
|
@@ -590,6 +591,14 @@ def _render_with_remotion( | |
| ] | ||
| if logo_path and os.path.exists(logo_path): | ||
| cmd.extend(["--logo", os.path.abspath(logo_path)]) | ||
| if name_card and name_card.get("title"): | ||
| cmd.extend(["--name-card", str(name_card["title"])]) | ||
| if name_card.get("subtitle"): | ||
| cmd.extend(["--name-card-sub", str(name_card["subtitle"])]) | ||
| if name_card.get("seconds"): | ||
| cmd.extend(["--name-card-seconds", str(name_card["seconds"])]) | ||
| if name_card.get("accent"): | ||
| cmd.extend(["--name-card-accent", str(name_card["accent"])]) | ||
| if keep_caption_overlay: | ||
| cmd.append("--keep-overlay") | ||
|
|
||
|
|
@@ -652,6 +661,8 @@ def generate_clip( | |
| logo_path: Optional[str] = None, | ||
| outro_path: Optional[str] = None, | ||
| intro_path: Optional[str] = None, | ||
| name_card: Optional[dict] = None, | ||
| bookend_fade: float = 0.0, | ||
| clean_fillers: bool = True, | ||
| keep_segments: list[dict] = None, | ||
| trim_opening: Optional[bool] = None, | ||
|
|
@@ -909,7 +920,8 @@ def generate_clip( | |
| caption_style=caption_style, | ||
| output_path=captioned_path, | ||
| time_offset=caption_time_offset, | ||
| logo_path=logo_path if (style_config.get("logo_support", False) and logo_path) else None, | ||
| logo_path=logo_path or None, | ||
| name_card=name_card, | ||
|
Comment on lines
+923
to
+924
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Do not silently drop
Render an equivalent name card in the fallback path. If that is not supported, reject this option combination instead of producing a clip that omits the requested overlay. 🤖 Prompt for AI Agents |
||
| keep_caption_overlay=keep_caption_overlay, | ||
| ) | ||
|
|
||
|
|
@@ -931,7 +943,7 @@ def generate_clip( | |
|
|
||
| use_gradient = style_config.get("gradient_overlay", False) | ||
| gradient_opacity = style_config.get("gradient_opacity", 0.6) | ||
| use_logo = style_config.get("logo_support", False) and logo_path | ||
| use_logo = bool(logo_path) | ||
|
|
||
| burn_captions( | ||
| input_path=cropped_path, | ||
|
|
@@ -970,15 +982,17 @@ def generate_clip( | |
| intro_scaled = os.path.join(work_dir, "intro_scaled.mp4") | ||
| scale_to_frame(intro_path, intro_scaled, cw, ch) | ||
| with_intro_path = os.path.join(work_dir, "with_intro.mp4") | ||
| concat_outro(intro_scaled, final_video_path, with_intro_path) | ||
| concat_outro(intro_scaled, final_video_path, with_intro_path, | ||
| crossfade_duration=bookend_fade) | ||
| final_video_path = with_intro_path | ||
|
|
||
| if outro_path and os.path.exists(outro_path): | ||
| if progress_callback: | ||
| progress_callback(85, f"Adding outro ({total_steps}/{total_steps})") | ||
|
|
||
| with_outro_path = os.path.join(work_dir, "with_outro.mp4") | ||
| concat_outro(final_video_path, outro_path, with_outro_path) | ||
| concat_outro(final_video_path, outro_path, with_outro_path, | ||
| crossfade_duration=bookend_fade) | ||
| final_video_path = with_outro_path | ||
|
|
||
| # Step 6: Move to output | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,90 @@ | ||||||||||||||||||
| import React from "react"; | ||||||||||||||||||
| import { useCurrentFrame, useVideoConfig } from "remotion"; | ||||||||||||||||||
| import { captionScale } from "../types"; | ||||||||||||||||||
| import { fadeInOut } from "../motion"; | ||||||||||||||||||
|
|
||||||||||||||||||
| export interface NameCardProps { | ||||||||||||||||||
| /** Who is speaking, and what they are. One line each. */ | ||||||||||||||||||
| title: string; | ||||||||||||||||||
| subtitle?: string; | ||||||||||||||||||
| /** How long it stays, from the top of the clip. */ | ||||||||||||||||||
| seconds?: number; | ||||||||||||||||||
| background?: string; | ||||||||||||||||||
| color?: string; | ||||||||||||||||||
| accent?: string; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * The lower third that says who this is. | ||||||||||||||||||
| * | ||||||||||||||||||
| * A clip lifted out of an hour of conversation opens on a stranger. Every | ||||||||||||||||||
| * show solves it the same way and podcli had no answer at all, so the name | ||||||||||||||||||
| * card was drawn somewhere else and burned in by hand. | ||||||||||||||||||
| * | ||||||||||||||||||
| * Anchored to the bottom rather than centred, sized off the composition the | ||||||||||||||||||
| * way captions are, and gone by the time anyone would tire of it. | ||||||||||||||||||
| */ | ||||||||||||||||||
| export const NameCard: React.FC<NameCardProps> = ({ | ||||||||||||||||||
| title, | ||||||||||||||||||
| subtitle, | ||||||||||||||||||
| seconds = 3, | ||||||||||||||||||
| background = "rgba(0,0,0,0.85)", | ||||||||||||||||||
| color = "#FFFFFF", | ||||||||||||||||||
| accent = "#2ED9C3", | ||||||||||||||||||
| }) => { | ||||||||||||||||||
| const frame = useCurrentFrame(); | ||||||||||||||||||
| const { fps, height } = useVideoConfig(); | ||||||||||||||||||
| const s = captionScale(height); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!title) return null; | ||||||||||||||||||
|
|
||||||||||||||||||
| const opacity = fadeInOut({ | ||||||||||||||||||
| frame, fps, start: 0, end: seconds, inFrames: 8, outFrames: 10, | ||||||||||||||||||
| }); | ||||||||||||||||||
| if (opacity <= 0) return null; | ||||||||||||||||||
|
Comment on lines
+41
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Unmount the card after its configured duration. If Check the current frame against Proposed fix const opacity = fadeInOut({
frame, fps, start: 0, end: seconds, inFrames: 8, outFrames: 10,
});
- if (opacity <= 0) return null;
+ if (frame >= Math.round(seconds * fps) || opacity <= 0) return null;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| return ( | ||||||||||||||||||
| <div | ||||||||||||||||||
| style={{ | ||||||||||||||||||
| position: "absolute", | ||||||||||||||||||
| left: 0, | ||||||||||||||||||
| bottom: 620 * s, | ||||||||||||||||||
| maxWidth: "78%", | ||||||||||||||||||
| padding: `${18 * s}px ${28 * s}px ${16 * s}px`, | ||||||||||||||||||
| background, | ||||||||||||||||||
| borderBottom: `${10 * s}px solid ${accent}`, | ||||||||||||||||||
| opacity, | ||||||||||||||||||
| // Rises the last few pixels as it arrives, which reads as arriving | ||||||||||||||||||
| // rather than appearing. | ||||||||||||||||||
| transform: `translateY(${(1 - opacity) * 12 * s}px)`, | ||||||||||||||||||
| }} | ||||||||||||||||||
| > | ||||||||||||||||||
| <div | ||||||||||||||||||
| style={{ | ||||||||||||||||||
| fontFamily: "'DM Sans', sans-serif", | ||||||||||||||||||
| fontSize: 44 * s, | ||||||||||||||||||
| fontWeight: 700, | ||||||||||||||||||
| lineHeight: 1.2, | ||||||||||||||||||
| color, | ||||||||||||||||||
| }} | ||||||||||||||||||
| > | ||||||||||||||||||
| {title} | ||||||||||||||||||
| </div> | ||||||||||||||||||
| {subtitle && ( | ||||||||||||||||||
| <div | ||||||||||||||||||
| style={{ | ||||||||||||||||||
| fontFamily: "'DM Sans', sans-serif", | ||||||||||||||||||
| fontSize: 38 * s, | ||||||||||||||||||
| fontWeight: 400, | ||||||||||||||||||
| lineHeight: 1.25, | ||||||||||||||||||
| marginTop: 4 * s, | ||||||||||||||||||
| color, | ||||||||||||||||||
| opacity: 0.85, | ||||||||||||||||||
| }} | ||||||||||||||||||
| > | ||||||||||||||||||
| {subtitle} | ||||||||||||||||||
| </div> | ||||||||||||||||||
| )} | ||||||||||||||||||
| </div> | ||||||||||||||||||
| ); | ||||||||||||||||||
| }; | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not overwrite the preset fade when the option is absent.
--bookend-fadedefaults to0.0. Therefore Line 585 always replaces a presetbookend_fadevalue with a hard cut.Use
default=Nonein the argument definition. Keep the existingis not Nonecheck so only an explicit CLI option overrides the preset.Proposed fix
🤖 Prompt for AI Agents