/** * A generated module for TestCard functions * * This module has been generated via dagger init and serves as a reference to * basic module structure as you get started with Dagger. * * Two functions have been pre-created. You can modify, delete, or add to them, * as needed. They demonstrate usage of arguments and return types using simple * echo and grep commands. The functions can be called from the dagger CLI or * from one of the SDKs. * * The first line in this comment block is a short description line and the * rest is a long description with more detail on the module's purpose or usage, * if appropriate. All modules should have a short description. */ import { dag, Container, Directory, File, Secret, object, func, argument, Platform } from '@dagger.io/dagger'; @object() export class TestCard { // ============ Helpers ============ private auxMediaBase(): Container { // Equivalent of Earthly target: aux-media return dag .container() .from('debian:bookworm') .withExec([ 'bash', '-lc', 'apt-get update && apt-get install -y ffmpeg sox && rm -rf /var/lib/apt/lists/*' ]); } private nodeWithBunBase(platform?: Platform): Container { return dag.container({ platform }).from('node:lts').withExec(['npm', 'install', '-g', 'bun']); } // ============ site ============ /** * Build the website (equivalent to Earthly target `site`). * Returns the built `build/` directory as a Dagger Directory. */ @func() async site(@argument({ defaultPath: '/' }) source: Directory): Promise { const assets = await this.assetsGenerated(source); const ctr = this.nodeWithBunBase() .withMountedDirectory('/src', source) .withMountedDirectory('/assets-generated', assets) .withWorkdir('/src') .withExec(['bun', 'install', '--frozen-lockfile']) .withExec([ 'bash', '-lc', 'mkdir -p /src/assets/generated && cp -a /assets-generated/. /src/assets/generated' ]) .withExec([ 'sh', '-lc', 'export VITE_BUILD_DATE=$(date -Iminutes -u | sed "s/+00:00//") && bun x svelte-kit sync && bun run build' ]); return ctr.directory('/src/build'); } // ============ deploy ============ /** * Deploy the built site via rsync over SSH (equivalent to Earthly target `deploy`). * Provide secrets corresponding to SSH config and target. */ @func() async deploy( sshConfig: Secret, sshUploadKey: Secret, sshKnownHosts: Secret, sshTarget: Secret, @argument({ defaultPath: '/build' }) build: Directory ): Promise { const ctr = dag .container() .from('alpine:latest') .withExec(['sh', '-lc', 'apk add --no-cache openssh-client rsync']) .withMountedDirectory('/build', build) .withSecretVariable('SSH_CONFIG', sshConfig) .withSecretVariable('SSH_UPLOAD_KEY', sshUploadKey) .withSecretVariable('SSH_KNOWN_HOSTS', sshKnownHosts) .withSecretVariable('SSH_TARGET', sshTarget) .withExec([ 'sh', '-lc', 'mkdir -p "$HOME/.ssh" && echo "$SSH_CONFIG" > $HOME/.ssh/config && echo "$SSH_UPLOAD_KEY" > $HOME/.ssh/id_rsa && echo "$SSH_KNOWN_HOSTS" > $HOME/.ssh/known_hosts && chmod 600 $HOME/.ssh/*' ]) .withExec(['sh', '-lc', 'rsync -cvrz --delete /build/ "$SSH_TARGET"']); return ctr.stdout(); } // ============ avsync-video-components ============ /** * Render AV sync frames with Bun. Returns the frames directory. * Mirrors the `avsync-video-components` video part. */ @func() async avsyncFrames( fps: number = 60, size: number = 1200, @argument({ defaultPath: '/' }) source: Directory ): Promise { const ctr = this.nodeWithBunBase() // pptr troubleshooting libs .withExec([ 'bash', '-lc', 'apt-get update && apt-get -y install chromium libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 && rm -rf /var/lib/apt/lists/*' ]) .withExec([ 'bash', '-lc', 'groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser && mkdir -p /home/pptruser && chown -R pptruser:pptruser /home/pptruser' ]) .withUser('pptruser') .withDirectory('/site', source, { owner: 'pptruser', include: ['package.json', 'bun.lock'] }) .withWorkdir('/site') .withExec(['bun', 'install', '--frozen-lockfile', '--verbose']) .withEnvVariable('PUPPETEER_EXECUTABLE_PATH', '/usr/bin/chromium') .withEnvVariable('PUPPETEER_SKIP_DOWNLOAD', '1') .withDirectory('/site', source, { owner: 'pptruser' }) .withExec([ 'bun', 'av:render:video', '--fps', String(fps), '--cycles', '1', '--size', String(size), '--output', '/var/tmp/frames' ]); return ctr.directory('/var/tmp/frames'); } /** * Render AV sync audio track with Bun. Returns the WAV track file. * Mirrors the `avsync-video-components` audio part. */ @func() async avsyncTrack( cycles: number = 16, @argument({ defaultPath: '/' }) source: Directory ): Promise { const ctr = this.nodeWithBunBase() .withDirectory('/site', source) .withWorkdir('/site') .withExec(['bun', 'install', '--frozen-lockfile']) .withExec([ 'bun', 'av:render:audio', '-i', 'beep.wav', '-o', '/var/tmp/track.wav', '--repeats', String(cycles) ]); return ctr.file('/var/tmp/track.wav'); } // ============ avsync-video ============ /** * Compose frames and audio track into avsync.webm. Returns the resulting file. * Mirrors the Earthly `avsync-video` target. */ @func() async avsyncVideo( fps: number = 60, cycles: number = 16, @argument({ defaultPath: '/' }) source: Directory ): Promise { const frames = await this.avsyncFrames(fps, 1200, source); const track = await this.avsyncTrack(cycles, source); const ctr = this.auxMediaBase() .withMountedDirectory('/frames', frames) .withMountedFile('/track.wav', track) .withExec(['sh', '-lc', 'find /frames -type f | sort | sed "s#^#file #" > /frames.txt']) .withEnvVariable('CYCLES', String(cycles)) .withExec([ 'sh', '-lc', 'for i in $(seq 1 $CYCLES); do cat /frames.txt >> /final-frames.txt; done' ]) .withEnvVariable('FPS', String(fps)) .withExec([ 'sh', '-lc', 'ffmpeg -r "$FPS" -f concat -safe 0 -i /final-frames.txt -i /track.wav -c:v libvpx-vp9 -pix_fmt yuva420p -shortest /avsync.webm' ]); return ctr.file('/avsync.webm'); } // ============ audio-channel-tracks (WAV) ============ /** * Generate stereo/5.1/7.1 WAV channel tracks from assets/audio/channels input. * Mirrors the Earthly `audio-channel-tracks` target. */ @func() async audioChannelTracksWav(@argument({ defaultPath: '/' }) src: Directory): Promise { const ctr = this.auxMediaBase() .withMountedDirectory('/raw', src.directory('/assets/audio/channels')) .withExec([ 'bash', '-lc', 'mkdir -p /input /output && cd /raw && for file in *.wav; do sox "$file" "/input/$file" silence 1 0.1 0.1% reverse silence 1 0.1 0.1% reverse; done' ]) .withWorkdir('/input') .withExec(['bash', '-lc', 'mkdir -p /output/wav/stereo /output/wav/5.1 /output/wav/7.1']) // stereo .withExec([ 'bash', '-lc', 'ffmpeg -i Left.wav -af "pan=stereo|FL=c0" /output/wav/stereo/Left.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Right.wav -af "pan=stereo|FR=c0" /output/wav/stereo/Right.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Center.wav -af "pan=stereo|FL=c0|FR=c0" /output/wav/stereo/Center.wav -hide_banner -loglevel error' ]) // 5.1 .withExec([ 'bash', '-lc', 'ffmpeg -i Front_Left.wav -af "pan=5.1|FL=c0" /output/wav/5.1/Front_Left.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Front_Right.wav -af "pan=5.1|FR=c0" /output/wav/5.1/Front_Right.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Front_Center.wav -af "pan=5.1|FC=c0" /output/wav/5.1/Front_Center.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Noise.wav -af "pan=5.1|LFE=c0" /output/wav/5.1/LFE_Noise.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Rear_Left.wav -af "pan=5.1|BL=c0" /output/wav/5.1/Rear_Left.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Rear_Right.wav -af "pan=5.1|BR=c0" /output/wav/5.1/Rear_Right.wav -hide_banner -loglevel error' ]) // 7.1 .withExec([ 'bash', '-lc', 'ffmpeg -i Front_Left.wav -af "pan=7.1|FL=c0" /output/wav/7.1/Front_Left.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Front_Right.wav -af "pan=7.1|FR=c0" /output/wav/7.1/Front_Right.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Front_Center.wav -af "pan=7.1|FC=c0" /output/wav/7.1/Front_Center.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Noise.wav -af "pan=7.1|LFE=c0" /output/wav/7.1/LFE_Noise.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Side_Left.wav -af "pan=7.1|SL=c0" /output/wav/7.1/Side_Left.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Side_Right.wav -af "pan=7.1|SR=c0" /output/wav/7.1/Side_Right.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Rear_Left.wav -af "pan=7.1|BL=c0" /output/wav/7.1/Rear_Left.wav -hide_banner -loglevel error' ]) .withExec([ 'bash', '-lc', 'ffmpeg -i Rear_Right.wav -af "pan=7.1|BR=c0" /output/wav/7.1/Rear_Right.wav -hide_banner -loglevel error' ]); return ctr.directory('/output/wav'); } // ============ audio-channel-tracks-ogg ============ @func() async audioChannelTracksOgg( @argument({ defaultPath: '/' }) source: Directory ): Promise { const wavDir = await this.audioChannelTracksWav(source); const ctr = this.auxMediaBase() .withMountedDirectory('/output/wav', wavDir) .withExec(['bash', '-lc', 'mkdir -p /output/ogg/stereo /output/ogg/5.1 /output/ogg/7.1']) .withExec([ 'bash', '-lc', 'for file in /output/wav/stereo/*.wav; do ffmpeg -i "$file" -c:a libvorbis "/output/ogg/stereo/$(basename "$file" .wav).ogg" -hide_banner -loglevel error; done' ]) .withExec([ 'bash', '-lc', 'for file in /output/wav/5.1/*.wav; do ffmpeg -i "$file" -c:a libvorbis "/output/ogg/5.1/$(basename "$file" .wav).ogg" -hide_banner -loglevel error; done' ]) .withExec([ 'bash', '-lc', 'for file in /output/wav/7.1/*.wav; do ffmpeg -i "$file" -c:a libvorbis "/output/ogg/7.1/$(basename "$file" .wav).ogg" -hide_banner -loglevel error; done' ]); return ctr.directory('/output/ogg'); } // ============ audio-channel-tracks-mp3 ============ @func() async audioChannelTracksMp3( @argument({ defaultPath: '/' }) source: Directory ): Promise { const wavDir = await this.audioChannelTracksWav(source); const ctr = this.auxMediaBase() .withMountedDirectory('/output/wav', wavDir) .withExec(['bash', '-lc', 'mkdir -p /output/mp3/stereo /output/mp3/5.1 /output/mp3/7.1']) .withExec([ 'bash', '-lc', 'for file in /output/wav/stereo/*.wav; do ffmpeg -i "$file" -c:a libmp3lame "/output/mp3/stereo/$(basename "$file" .wav).mp3" -hide_banner -loglevel error; done' ]) .withExec([ 'bash', '-lc', 'for file in /output/wav/5.1/*.wav; do ffmpeg -i "$file" -c:a libmp3lame "/output/mp3/5.1/$(basename "$file" .wav).mp3" -hide_banner -loglevel error; done' ]) .withExec([ 'bash', '-lc', 'for file in /output/wav/7.1/*.wav; do ffmpeg -i "$file" -c:a libmp3lame "/output/mp3/7.1/$(basename "$file" .wav).mp3" -hide_banner -loglevel error; done' ]); return ctr.directory('/output/mp3'); } // ============ assets-generated ============ /** * Aggregate generated assets into a directory containing: * - avsync.webm * - audio/ (MP3 variants) */ @func() async assetsGenerated(@argument({ defaultPath: '/' }) source: Directory): Promise { const avsync = await this.avsyncVideo(60, 16, source); const audioMp3 = await this.audioChannelTracksMp3(source); return dag.directory().withFile('avsync.webm', avsync).withDirectory('audio', audioMp3); } }