Claude Skill

ugc-fixloop

the UGC fix-loop toolkit — surgically re-render a bad window/beat of a single-take UGC master (stitch_replacement.py, pure FFmpeg) and GPT cross-model review a Seedance prompt before render (vet_seedance_prompt.py, routed through the openai-proxy). Fetch it into a one-shot UGC re

LLM Mart · 0 points · 6 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download gooseworks-ai-goose-skills-skills_ads_capabilities_ugc-fixloop-e1592ee.zip · 8 KB
Part of gooseworks-ai/goose-skills — 44 skills

Install

skills CLI npx skills add https://github.com/gooseworks-ai/goose-skills/tree/main/skills/ads/capabilities/ugc-fixloop
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install gooseworks-ai-goose-skills@llmmart
Git git clone https://github.com/gooseworks-ai/goose-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole gooseworks-ai/goose-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

ugc-fixloop

The UGC fix-loop toolkit. The one-shot UGC video recipes (create-ugc-*-video-from-refs) render a single continuous Seedance 2.0 reference-to-video master with native lip-synced audio. This capability ships the two scripts those recipes run, so they exist on the remote machine (fetched into /tmp/gooseworks-scripts/ugc-fixloop/).

Any re-render of a replacement clip goes through the same proxy path the recipe uses for the take (create-video-fal / fal-proxy), NEVER a direct fal.run call.

Env / deps

  • stitch_replacement.py — no API key, no network. Needs ffmpeg + ffprobe on PATH (all local FFmpeg).
  • vet_seedance_prompt.py — routes through the GooseWorks openai-proxy (<api_base>/api/internal/openai-proxy/v1/chat/completions), reading creds from ~/.gooseworks/credentials.json — no direct OpenAI call, no local key; the call bills the Ads agent. Exits 3 if the proxy/creds are unavailable so the recipe can fall back to an inline self-review (the vet is advisory, not a gate).

Run — vet_seedance_prompt.py (GPT cross-model prompt review)

A deliberately NON-Claude second opinion on the Seedance prompt before you spend the render (Claude reviewing its own prompt is a weaker signal). Takes the prompt as an argument:

vet_seedance_prompt.py --prompt-file working/seedance-prompt.txt \
    [--brief "one-line intent"] [--refs "@Image1=avatar; @Image2=product; @Image3=env"] \
    [--words 28] [--out working/seedance-review.md]

Prints + saves the structured review (verdict, line edits, word budget, consistency risk).

Run — stitch_replacement.py (surgical beat/window swap, deterministic)

Replaces one segment of the master on the VIDEO track only; the master's audio (VO + ambience) plays straight through, so lip-sync on talking beats is never touched. Output is re-encoded H.264 / yuv420p at the master's fps + resolution.

Required: --master M.mp4 --replacement R.mp4 --output O.mp4. Pick the window ONE of two ways:

# By beat (1-indexed segment between auto-detected scene cuts):
stitch_replacement.py --master M.mp4 --replacement R.mp4 --output O.mp4 --replace-beat 2

# By explicit window (seconds):
stitch_replacement.py --master M.mp4 --replacement R.mp4 --output O.mp4 \
    --window-start 4.21 --window-end 8.75 --fit stretch

All args:

  • --master (required) — the single-take master mp4.
  • --replacement (required) — the re-rendered silent replacement clip (generated via create-video-fal).
  • --output (required) — output mp4 path.
  • --window-start / --window-end (float seconds) — explicit hole to replace.
  • --replace-beat (int, 1-indexed) — pick the segment between detected scene cuts.
  • --scene-threshold (float, default 0.3) — scene-cut sensitivity for --replace-beat.
  • --fit {stretch,trim,freeze} (default stretch) — reconcile replacement length to the hole.
  • --dry-run — print the ffmpeg command without running.

Warns if output duration drifts >0.15s from the master (audio-sync check).

Files (goose-skills)
  • scripts
    • stitch_replacement.py 6.4 KB
      #!/usr/bin/env python3
      """Surgically replace one segment of a master UGC clip with a re-rendered
      replacement clip, preserving the master's continuous audio.
      
      This is the deterministic core of the create-ugc-product-video-from-refs fix
      loop. The master is a single Seedance reference-to-video render with native
      lip-synced audio. When one internal beat drifts (a bad swing, a morphing
      product, a contorted body), we re-render JUST that beat as a short silent clip
      and swap it in on the VIDEO TRACK ONLY — the master's audio (VO + ambience)
      plays straight through, so lip-sync on the talking beats is never touched.
      
      Two ways to choose the window to replace:
        1. Explicit:   --window-start S --window-end E   (seconds)
        2. By beat:    --replace-beat N                    (1-indexed segment between
                       scene cuts; cuts are auto-detected with --scene-threshold)
      
      The replacement is almost never exactly the hole length. --fit reconciles it:
        stretch (default) — time-scale the replacement to fill the hole exactly
                            (slight slow/fast motion; flatters athletic motion)
        trim              — cut the replacement to the hole length (drops the tail)
        freeze            — play the replacement, then hold its last frame to fill
      
      Output is re-encoded H.264 / yuv420p at the master's fps and resolution so the
      seams are clean and the file plays everywhere.
      
      Usage:
        stitch_replacement.py --master M.mp4 --replacement R.mp4 --output O.mp4 \
            --replace-beat 2
        stitch_replacement.py --master M.mp4 --replacement R.mp4 --output O.mp4 \
            --window-start 4.21 --window-end 8.75 --fit stretch
      """
      from __future__ import annotations
      
      import argparse
      import json
      import subprocess
      import sys
      
      
      def run(cmd: list[str]) -> str:
          p = subprocess.run(cmd, capture_output=True, text=True)
          if p.returncode != 0:
              sys.exit(f"FATAL: command failed: {' '.join(cmd)}\n{p.stderr}")
          return p.stdout
      
      
      def probe(path: str) -> dict:
          out = run([
              "ffprobe", "-v", "error", "-select_streams", "v:0",
              "-show_entries", "stream=width,height,r_frame_rate",
              "-show_entries", "format=duration", "-of", "json", path,
          ])
          d = json.loads(out)
          s = d["streams"][0]
          num, den = s["r_frame_rate"].split("/")
          return {
              "w": int(s["width"]),
              "h": int(s["height"]),
              "fps": float(num) / float(den),
              "dur": float(d["format"]["duration"]),
          }
      
      
      def scene_cuts(path: str, threshold: float) -> list[float]:
          """Return sorted internal cut timestamps (excludes 0 and end)."""
          p = subprocess.run(
              ["ffmpeg", "-nostdin", "-hide_banner", "-i", path,
               "-filter_complex", f"select='gt(scene,{threshold})',metadata=print:file=-",
               "-an", "-f", "null", "-"],
              capture_output=True, text=True,
          )
          out = (p.stdout or "") + "\n" + (p.stderr or "")  # file=- can land on either
          cuts = []
          for line in out.splitlines():
              if "pts_time:" in line:
                  try:
                      cuts.append(float(line.split("pts_time:")[1].split()[0]))
                  except (IndexError, ValueError):
                      pass
          return sorted(cuts)
      
      
      def main() -> int:
          ap = argparse.ArgumentParser()
          ap.add_argument("--master", required=True)
          ap.add_argument("--replacement", required=True)
          ap.add_argument("--output", required=True)
          ap.add_argument("--window-start", type=float)
          ap.add_argument("--window-end", type=float)
          ap.add_argument("--replace-beat", type=int,
                          help="1-indexed segment between detected scene cuts")
          ap.add_argument("--scene-threshold", type=float, default=0.3)
          ap.add_argument("--fit", choices=["stretch", "trim", "freeze"], default="stretch")
          ap.add_argument("--dry-run", action="store_true")
          args = ap.parse_args()
      
          m = probe(args.master)
          r = probe(args.replacement)
          total = m["dur"]
      
          # Resolve the replacement window.
          if args.window_start is not None and args.window_end is not None:
              start, end = args.window_start, args.window_end
          elif args.replace_beat is not None:
              cuts = scene_cuts(args.master, args.scene_threshold)
              bounds = [0.0] + cuts + [total]
              n = args.replace_beat
              if n < 1 or n >= len(bounds):
                  sys.exit(f"FATAL: --replace-beat {n} out of range; "
                           f"detected {len(bounds)-1} segments at cuts {cuts}")
              start, end = bounds[n - 1], bounds[n]
              print(f"[stitch] detected cuts {', '.join(f'{c:.2f}' for c in cuts)}")
              print(f"[stitch] beat {n} → window {start:.2f}-{end:.2f}s")
          else:
              sys.exit("FATAL: pass --window-start/--window-end or --replace-beat")
      
          hole = end - start
          if hole <= 0:
              sys.exit(f"FATAL: empty window {start:.2f}-{end:.2f}")
          print(f"[stitch] hole={hole:.3f}s  replacement={r['dur']:.3f}s  fit={args.fit}")
      
          # Build the replacement video filter to fit the hole exactly.
          if args.fit == "stretch":
              factor = hole / r["dur"]
              rep = f"trim=0:{r['dur']:.3f},setpts=(PTS-STARTPTS)*{factor:.5f}"
          elif args.fit == "trim":
              rep = f"trim=0:{min(hole, r['dur']):.3f},setpts=PTS-STARTPTS"
          else:  # freeze
              rep = (f"trim=0:{min(hole, r['dur']):.3f},setpts=PTS-STARTPTS,"
                     f"tpad=stop_mode=clone:stop_duration={max(0.0, hole - r['dur']):.3f}")
      
          W, H, FPS = m["w"], m["h"], m["fps"]
          sc = f"scale={W}:{H},setsar=1,fps={FPS:g}"
          fc = (
              f"[0:v]trim=0:{start:.3f},setpts=PTS-STARTPTS,{sc}[a];"
              f"[1:v]{rep},{sc}[b];"
              f"[0:v]trim={end:.3f}:{total:.3f},setpts=PTS-STARTPTS,{sc}[c];"
              f"[a][b][c]concat=n=3:v=1:a=0[outv]"
          )
      
          cmd = [
              "ffmpeg", "-nostdin", "-loglevel", "error", "-y",
              "-i", args.master, "-i", args.replacement,
              "-filter_complex", fc,
              "-map", "[outv]", "-map", "0:a?",
              "-c:v", "libx264", "-pix_fmt", "yuv420p", "-r", f"{FPS:g}",
              "-c:a", "aac", "-b:a", "192k",
              args.output,
          ]
          if args.dry_run:
              print("[stitch] DRY RUN — would run:\n  " + " ".join(cmd))
              return 0
      
          run(cmd)
          o = probe(args.output)
          print(f"[stitch] wrote {args.output}  {o['w']}x{o['h']} {o['fps']:g}fps {o['dur']:.2f}s")
          if abs(o["dur"] - total) > 0.15:
              print(f"[stitch] WARNING: output {o['dur']:.2f}s vs master {total:.2f}s "
                    f"(>0.15s drift — check audio sync)")
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • vet_seedance_prompt.py 5.2 KB
      #!/usr/bin/env python3
      """Cross-model review of a Seedance 2.0 reference-to-video prompt by GPT — a
      deliberately NON-Claude second opinion before you spend the render.
      
      Portable: routes through the GooseWorks **openai-proxy** (bills the Ads agent, real
      key never touches this machine) — never a direct api.openai.com call. Reads creds
      from ~/.gooseworks/credentials.json (the CLI writes it). Takes the prompt to review
      as an ARGUMENT (nothing baked in).
      
        vet_seedance_prompt.py --prompt-file working/seedance-prompt.txt \
            [--brief "one-line intent"] [--refs "@Image1=avatar; @Image2=product; @Image3=env"] \
            [--words 28] [--model gpt-5.5] [--out working/seedance-review.md]
      
      Exit 0 + prints/saves the review. Exit 3 if the proxy/creds are unavailable (so the
      recipe can fall back to an inline self-review — the review is advisory, not a gate).
      """
      import argparse
      import json
      import os
      import pathlib
      import sys
      import urllib.request
      import urllib.error
      from urllib.parse import urlencode
      
      SYSTEM_PROMPT = """\
      You are a senior AI-video prompt engineer who has shipped hundreds of short-form UGC
      ads on ByteDance Seedance 2.0, Kling, and Veo. You know Seedance 2.0 reference-to-video
      intimately: how it binds reference images addressed as @Image1/@Image2/@Image3, native
      lip-synced dialogue (generate_audio=true), where it drifts (identity, product geometry,
      small text, multi-cut consistency over a single ~15s render), and how spoken-word budget
      maps to clip duration. You have strong opinions and you don't hedge — when you spot a
      problem you name it and give the specific rewrite, not vague encouragement."""
      
      USER_TEMPLATE = """\
      Review this Seedance 2.0 reference-to-video prompt for a single vertical (9:16) UGC ad
      before we render it. {brief}{refs}Native lip-sync means a tight spoken-word budget —
      our rule of thumb is ~{words} words for a ~15s clip; push back if you disagree.
      
      # The Seedance prompt to review
      ---
      {prompt}
      ---
      
      # What I want (be concrete; don't soft-pedal — if it will produce mush, say why)
      1. **Verdict** — ship-as-is / ship-with-tweaks / needs-rewrite, one sentence.
      2. **Specific line edits** — quote the phrase, give the replacement, why in <=15 words. Focus on
         what changes Seedance's output: @Image ref binding, camera-switch language, how hard cuts are
         signalled, consistency anchors, wording that invites drift. 0-8 edits.
      3. **Word/duration budget** — will every spoken line land at ~15s with the cuts, or cut a line? which?
      4. **Consistency risk** — rank the top 3 things most likely to drift across cuts (face, colors,
         product geometry, small text, camera switch) + the single best prompt-side mitigation for each.
      5. **Reusability note** — 1-2 rules to bake into the general prompt recipe so the next brief inherits them."""
      
      
      def _cfg():
          p = pathlib.Path(os.path.expanduser("~/.gooseworks/credentials.json"))
          if not p.exists():
              sys.exit(3)  # no creds → recipe falls back to inline self-review
          c = json.loads(p.read_text())
          return c["api_base"].rstrip("/"), c["api_key"], c.get("agent_id")
      
      
      def main():
          ap = argparse.ArgumentParser()
          ap.add_argument("--prompt-file", help="path to the Seedance prompt to review")
          ap.add_argument("--prompt", help="the Seedance prompt inline (alternative to --prompt-file)")
          ap.add_argument("--brief", default="")
          ap.add_argument("--refs", default="")
          ap.add_argument("--words", type=int, default=28)
          ap.add_argument("--model", default="gpt-5.5")
          ap.add_argument("--out", default="working/seedance-review.md")
          a = ap.parse_args()
      
          prompt = a.prompt or (pathlib.Path(a.prompt_file).read_text() if a.prompt_file else "")
          if not prompt.strip():
              sys.exit("give the prompt via --prompt-file or --prompt")
      
          api_base, tok, agent = _cfg()
          user = USER_TEMPLATE.format(
              prompt=prompt.strip(), words=a.words,
              brief=(a.brief.strip() + " ") if a.brief else "",
              refs=("Reference slots: " + a.refs.strip() + ". ") if a.refs else "",
          )
          body = {"model": a.model, "messages": [
              {"role": "system", "content": SYSTEM_PROMPT},
              {"role": "user", "content": user},
          ]}
          q = urlencode({"token": tok, **({"agent_id": agent} if agent else {})})
          url = f"{api_base}/api/internal/openai-proxy/v1/chat/completions?{q}"
          req = urllib.request.Request(url, data=json.dumps(body).encode(),
                                       headers={"Content-Type": "application/json"}, method="POST")
          try:
              with urllib.request.urlopen(req, timeout=180) as resp:
                  out = json.loads(resp.read().decode())
          except urllib.error.HTTPError as e:
              print(f"HTTP {e.code} {e.reason}\n{e.read().decode(errors='replace')}", file=sys.stderr)
              sys.exit(3)  # proxy error → advisory step, let the recipe self-review inline
          except Exception as e:
              print(f"openai-proxy unreachable: {e}", file=sys.stderr)
              sys.exit(3)
      
          review = out["choices"][0]["message"]["content"]
          outp = pathlib.Path(a.out)
          outp.parent.mkdir(parents=True, exist_ok=True)
          outp.write_text(review)
          print(f"[vet] model={out.get('model')} usage={out.get('usage', {})} saved={outp}\n")
          print(review)
      
      
      if __name__ == "__main__":
          main()
      
  • tests
    • smoke-test.md 1.8 KB
      # ugc-fixloop smoke test
      
      ## Purpose
      Confirm the fix-loop toolkit fetches and both scripts are runnable on a fresh machine.
      
      ## Steps
      
      1. **Fetch the capability.** From a one-shot UGC recipe's fix-loop step, run:
         ```
         gooseworks fetch ugc-fixloop
         ```
         Expect the scripts under `/tmp/gooseworks-scripts/ugc-fixloop/scripts/`.
      
      2. **stitch_replacement.py present + arg-parses.**
         ```
         python3 /tmp/gooseworks-scripts/ugc-fixloop/scripts/stitch_replacement.py --help
         ```
         Expect the usage block listing `--master`, `--replacement`, `--output`, `--window-start`, `--window-end`, `--replace-beat`, `--scene-threshold`, `--fit`, `--dry-run`. Requires `ffmpeg` + `ffprobe` on PATH (no API key).
      
      3. **stitch dry-run** (with any two short mp4s):
         ```
         python3 /tmp/gooseworks-scripts/ugc-fixloop/scripts/stitch_replacement.py \
           --master M.mp4 --replacement R.mp4 --output O.mp4 --replace-beat 1 --dry-run
         ```
         Expect a printed ffmpeg command and exit 0; no file written.
      
      4. **vet_seedance_prompt.py present.**
         ```
         python3 /tmp/gooseworks-scripts/ugc-fixloop/scripts/vet_seedance_prompt.py
         ```
         On a machine WITHOUT `/Users/0xhbam/Desktop/Cursor/gtm-goose/.env` (cloud/CI/teammate), expect a clean `FATAL: ... not found` exit — this script is a direct-OpenAI, operator-machine-only helper and the fix loop should skip it there. On the operator machine with `OPENAI_API_KEY` set in that env file, expect a written `gpt55_seedance_review.md` and the review printed to stdout.
      
      ## Pass criteria
      - Both scripts resolve under `/tmp/gooseworks-scripts/ugc-fixloop/scripts/` after fetch.
      - `stitch_replacement.py --help` and `--dry-run` succeed with no network/API dependency.
      - `vet_seedance_prompt.py` either produces a review (key present) or exits FATAL cleanly (key/env absent) — it never blocks the fix loop.
      
  • SKILL.md 3.4 KB
    ---
    name: ugc-fixloop
    description: the UGC fix-loop toolkit — surgically re-render a bad window/beat of a single-take UGC master (stitch_replacement.py, pure FFmpeg) and GPT cross-model review a Seedance prompt before render (vet_seedance_prompt.py, routed through the openai-proxy). Fetch it into a one-shot UGC recipe so both scripts resolve on any machine and the vet call bills the Ads agent.
    status: active
    ---
    
    # ugc-fixloop
    
    The UGC fix-loop toolkit. The one-shot UGC video recipes (`create-ugc-*-video-from-refs`)
    render a single continuous Seedance 2.0 reference-to-video master with native lip-synced
    audio. This capability ships the two scripts those recipes run, so they exist on the remote
    machine (fetched into `/tmp/gooseworks-scripts/ugc-fixloop/`).
    
    > Any re-render of a replacement clip goes through the same proxy path the recipe uses for
    > the take (`create-video-fal` / fal-proxy), NEVER a direct `fal.run` call.
    
    ## Env / deps
    - **`stitch_replacement.py`** — no API key, no network. Needs **`ffmpeg` + `ffprobe` on PATH** (all local FFmpeg).
    - **`vet_seedance_prompt.py`** — routes through the GooseWorks **openai-proxy** (`<api_base>/api/internal/openai-proxy/v1/chat/completions`), reading creds from `~/.gooseworks/credentials.json` — **no direct OpenAI call, no local key**; the call **bills the Ads agent**. Exits 3 if the proxy/creds are unavailable so the recipe can fall back to an inline self-review (the vet is advisory, not a gate).
    
    ## Run — vet_seedance_prompt.py (GPT cross-model prompt review)
    A deliberately NON-Claude second opinion on the Seedance prompt before you spend the render
    (Claude reviewing its own prompt is a weaker signal). Takes the prompt as an argument:
    ```
    vet_seedance_prompt.py --prompt-file working/seedance-prompt.txt \
        [--brief "one-line intent"] [--refs "@Image1=avatar; @Image2=product; @Image3=env"] \
        [--words 28] [--out working/seedance-review.md]
    ```
    Prints + saves the structured review (verdict, line edits, word budget, consistency risk).
    
    ## Run — stitch_replacement.py (surgical beat/window swap, deterministic)
    Replaces one segment of the master on the VIDEO track only; the master's audio (VO + ambience)
    plays straight through, so lip-sync on talking beats is never touched. Output is re-encoded
    H.264 / yuv420p at the master's fps + resolution.
    
    Required: `--master M.mp4 --replacement R.mp4 --output O.mp4`. Pick the window ONE of two ways:
    ```
    # By beat (1-indexed segment between auto-detected scene cuts):
    stitch_replacement.py --master M.mp4 --replacement R.mp4 --output O.mp4 --replace-beat 2
    
    # By explicit window (seconds):
    stitch_replacement.py --master M.mp4 --replacement R.mp4 --output O.mp4 \
        --window-start 4.21 --window-end 8.75 --fit stretch
    ```
    All args:
    - `--master` (required) — the single-take master mp4.
    - `--replacement` (required) — the re-rendered silent replacement clip (generated via `create-video-fal`).
    - `--output` (required) — output mp4 path.
    - `--window-start` / `--window-end` (float seconds) — explicit hole to replace.
    - `--replace-beat` (int, 1-indexed) — pick the segment between detected scene cuts.
    - `--scene-threshold` (float, default `0.3`) — scene-cut sensitivity for `--replace-beat`.
    - `--fit {stretch,trim,freeze}` (default `stretch`) — reconcile replacement length to the hole.
    - `--dry-run` — print the ffmpeg command without running.
    
    Warns if output duration drifts >0.15s from the master (audio-sync check).
    
  • skill.meta.json 259 B
    {
      "slug": "ugc-fixloop",
      "category": "capabilities",
      "domain": "ads",
      "tags": ["ads"],
      "installation": {
        "base_command": "npx goose-skills install ugc-fixloop",
        "supports": ["claude", "cursor", "codex"]
      },
      "requires_skills": ["watch"]
    }
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related