Spaces:
Configuration error
Configuration error
AIGoose
fix(denoise): rewrite step 9 — extract audio only then remux, avoids ffmpeg 6.1 filter_complex video mapping bug
15ce6d7 | name: Render Testimonial Video | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| gdrive_file_id: | |
| description: 'Google Drive file ID of testimonial MOV' | |
| required: true | |
| default: '1f1qlgyLJSCQgpjrWflaqldsNE2fZ-_1X' | |
| participant_name: | |
| description: 'Participant name' | |
| required: false | |
| default: 'Peserta Training' | |
| participant_role: | |
| description: 'Participant role / org' | |
| required: false | |
| default: 'AI Transformation Series 2026' | |
| pull_quote: | |
| description: 'Pull quote (Indonesian)' | |
| required: false | |
| default: 'Training ini langsung bisa dipraktek dan hasilnya nyata' | |
| jobs: | |
| render: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v4 | |
| - name: Install system deps | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y ffmpeg python3-pip libass-dev fonts-liberation | |
| pip3 install gdown huggingface_hub requests --quiet | |
| fc-cache -fv 2>/dev/null || true | |
| - name: Setup Node.js 22 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Install HyperFrames + chrome-headless-shell | |
| run: | | |
| npm install -g hyperframes | |
| npx hyperframes telemetry disable || true | |
| SHELL_BIN=$(npx --yes @puppeteer/browsers install chrome-headless-shell@stable 2>&1 | tail -1 | awk '{print $NF}') | |
| [ ! -x "$SHELL_BIN" ] && SHELL_BIN=$(find ~/.cache/puppeteer -name "chrome-headless-shell" -executable 2>/dev/null | head -1) | |
| echo "HYPERFRAMES_BROWSER_PATH=$SHELL_BIN" >> $GITHUB_ENV | |
| echo "Browser: $SHELL_BIN" | |
| - name: Download MOV from Google Drive | |
| run: | | |
| mkdir -p assets project/assets | |
| python3 - <<'PYEOF' | |
| import gdown, os, sys | |
| fid = "${{ github.event.inputs.gdrive_file_id }}" | |
| out = "assets/raw.MOV" | |
| gdown.download(f"https://drive.google.com/uc?id={fid}", out, quiet=False) | |
| if not os.path.exists(out): | |
| sys.exit(1) | |
| print(f"Downloaded {os.path.getsize(out)/1024/1024:.1f} MB") | |
| PYEOF | |
| - name: Convert MOV to 720p MP4 | |
| run: | | |
| ffmpeg -y -i assets/raw.MOV \ | |
| -vf "scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280" \ | |
| -c:v libx264 -preset fast -crf 22 \ | |
| -c:a aac -b:a 128k -movflags +faststart \ | |
| assets/source.mp4 | |
| echo "Source: $(du -sh assets/source.mp4)" | |
| - name: Trim interviewer section | |
| run: | | |
| ffmpeg -y -i assets/source.mp4 \ | |
| -filter_complex "\ | |
| [0:v]trim=0:71,setpts=PTS-STARTPTS[v1];\ | |
| [0:a]atrim=0:71,asetpts=PTS-STARTPTS[a1];\ | |
| [0:v]trim=77:87,setpts=PTS-STARTPTS[v2];\ | |
| [0:a]atrim=77:87,asetpts=PTS-STARTPTS[a2];\ | |
| [v1][a1][v2][a2]concat=n=2:v=1:a=1[vout][aout]" \ | |
| -map "[vout]" -map "[aout]" \ | |
| -c:v libx264 -preset fast -crf 22 \ | |
| -c:a aac -b:a 128k -movflags +faststart \ | |
| assets/cut.mp4 | |
| echo "Cut: $(ffprobe -v quiet -show_entries format=duration \ | |
| -of default=noprint_wrappers=1:nokey=1 assets/cut.mp4 | cut -d. -f1)s" | |
| - name: Denoise audio (extract, filter, remux) | |
| run: | | |
| # Step A: extract audio only and apply anlmdn noise reduction | |
| ffmpeg -y -i assets/cut.mp4 \ | |
| -vn -af "anlmdn=s=7:p=0.002:r=0.002:m=15" \ | |
| -c:a aac -b:a 128k \ | |
| assets/audio_clean.aac | |
| # Step B: remux original video with clean audio | |
| ffmpeg -y \ | |
| -i assets/cut.mp4 \ | |
| -i assets/audio_clean.aac \ | |
| -map 0:v -map 1:a \ | |
| -c:v copy -c:a copy \ | |
| -shortest \ | |
| assets/denoised.mp4 | |
| echo "Denoised: $(du -sh assets/denoised.mp4)" | |
| - name: Set footage duration | |
| run: | | |
| DUR=$(ffprobe -v quiet -show_entries format=duration \ | |
| -of default=noprint_wrappers=1:nokey=1 assets/denoised.mp4 | xargs printf "%.0f") | |
| echo "FOOTAGE_DUR=$DUR" >> $GITHUB_ENV | |
| echo "Footage duration: ${DUR}s" | |
| - name: Transcribe and generate SRT captions | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| run: | | |
| mkdir -p project/assets | |
| python3 scripts/gen_captions.py \ | |
| --input assets/denoised.mp4 \ | |
| --srt-out assets/captions.srt | |
| head -20 assets/captions.srt || true | |
| - name: Burn captions into video | |
| run: | | |
| FONT_PATH=$(fc-list | grep -i "LiberationSans-Regular" | head -1 | cut -d: -f1) | |
| [ -z "$FONT_PATH" ] && FONT_PATH="/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf" | |
| echo "Font: $FONT_PATH" | |
| ffmpeg -y -i assets/denoised.mp4 \ | |
| -vf "subtitles=assets/captions.srt:force_style='FontName=Liberation Sans,FontSize=22,PrimaryColour=&H001a2744,OutlineColour=&H00FFFFFF,Outline=10,Shadow=0,Alignment=2,MarginV=120,Bold=1'" \ | |
| -c:v libx264 -preset fast -crf 22 \ | |
| -c:a copy \ | |
| assets/captioned.mp4 | |
| echo "Captioned: $(du -sh assets/captioned.mp4)" | |
| - name: Mix background music | |
| run: | | |
| DUR=$FOOTAGE_DUR | |
| # Generate lo-fi ambient tone (CC0 — no external download needed) | |
| ffmpeg -y -f lavfi \ | |
| -i "aevalsrc=0.06*sin(2*PI*220*t)+0.04*sin(2*PI*330*t)+0.03*sin(2*PI*110*t):s=44100" \ | |
| -af "volume=0.10,lowpass=f=600,highpass=f=60,aecho=0.6:0.3:60:0.3" \ | |
| -t "${DUR}" -q:a 6 -acodec libmp3lame \ | |
| assets/music_gen.mp3 | |
| ffmpeg -y \ | |
| -i assets/captioned.mp4 \ | |
| -stream_loop -1 -i assets/music_gen.mp3 \ | |
| -filter_complex \ | |
| "[1:a]atrim=0:${DUR},asetpts=PTS-STARTPTS,volume=0.10[music];\ | |
| [0:a][music]amix=inputs=2:duration=first:dropout_transition=2[aout]" \ | |
| -map "0:v" -map "[aout]" \ | |
| -c:v copy -c:a aac -b:a 128k \ | |
| assets/clean.mp4 | |
| TOTAL=$((DUR + 3)) | |
| echo "TOTAL_DUR=$TOTAL" >> $GITHUB_ENV | |
| echo "clean.mp4: $(du -sh assets/clean.mp4) | footage=${DUR}s total=${TOTAL}s" | |
| - name: Build project folder | |
| run: | | |
| cp assets/clean.mp4 project/assets/clean.mp4 | |
| python3 scripts/patch_composition.py \ | |
| --input compositions/testimonial.html \ | |
| --output project/index.html \ | |
| --name "${{ github.event.inputs.participant_name }}" \ | |
| --role "${{ github.event.inputs.participant_role }}" \ | |
| --quote "${{ github.event.inputs.pull_quote }}" \ | |
| --footage-dur "$FOOTAGE_DUR" \ | |
| --total-dur "$TOTAL_DUR" | |
| echo "Project:"; ls -lh project/ project/assets/ | |
| - name: Lint composition | |
| run: npx hyperframes lint project || true | |
| - name: Render (720x1280 @ 24fps) | |
| env: | |
| HYPERFRAMES_BROWSER_PATH: ${{ env.HYPERFRAMES_BROWSER_PATH }} | |
| run: | | |
| mkdir -p output | |
| TS=$(date +%Y%m%d-%H%M) | |
| OUT="output/bright-studio-${TS}.mp4" | |
| echo "OUTPUT_FILE=$OUT" >> $GITHUB_ENV | |
| npx hyperframes render project \ | |
| --output "$OUT" --width 720 --height 1280 --fps 24 --quality standard | |
| echo "Rendered: $OUT ($(du -sh $OUT | cut -f1))" | |
| - name: Upload to Hugging Face | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| PARTICIPANT_NAME: ${{ github.event.inputs.participant_name }} | |
| run: | | |
| python3 - <<'PYEOF' | |
| import os | |
| from huggingface_hub import HfApi | |
| api = HfApi(token=os.environ["HF_TOKEN"]) | |
| out = os.environ["OUTPUT_FILE"] | |
| fname = os.path.basename(out) | |
| api.upload_file(path_or_fileobj=out, | |
| path_in_repo=f"renders/{fname}", | |
| repo_id="AIgoose/video-renders", repo_type="dataset") | |
| url = f"https://huggingface.co/datasets/AIgoose/video-renders/resolve/main/renders/{fname}" | |
| print(f"Video ready: {url}") | |
| print(f"::notice title=Video Ready::{url}") | |
| PYEOF | |
| - name: Upload artifact (backup) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bright-studio-v3 | |
| path: output/*.mp4 | |
| retention-days: 14 | |