Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions media/video-intelligence/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
outputs/
data/sample.mp4

12 changes: 12 additions & 0 deletions media/video-intelligence/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Video Intelligence

An offline media-understanding workflow with a reproducible generated dataset.
It creates a multi-scene test video, probes its streams, detects scene changes,
extracts thumbnails, measures audio loudness, and produces a searchable timeline.
FFmpeg and FFprobe do the real media processing; no hosted vision API is used.
Minimal CI workers without FFmpeg exercise the same data contract through a
deterministic three-scene fixture; the provided container always uses FFmpeg.

Run `./run.sh`. The `prepare` activity generates `outputs/source.mp4`, making the
dataset reproducible on any machine with FFmpeg. Use a larger local video by
replacing that artifact before the remaining stages.
6 changes: 6 additions & 0 deletions media/video-intelligence/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY scripts/pipeline.py /app/scripts/pipeline.py
ENTRYPOINT ["python", "/app/scripts/pipeline.py"]

6 changes: 6 additions & 0 deletions media/video-intelligence/environment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
id: video-intelligence-environment
name: Video Intelligence local environment
driver: local
runtimes: [{id: local, type: local, containerRuntime: docker}]
resources: [{id: workstation, runtimeId: local, cpuCores: 4, memoryBytes: 4294967296}]

5 changes: 5 additions & 0 deletions media/video-intelligence/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd);rm -rf "$root/outputs"
python3 "$root/scripts/pipeline.py" --stage all --output "$root/outputs";"$root/validate.sh"

4 changes: 4 additions & 0 deletions media/video-intelligence/scope.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
id: video-intelligence-scope
name: Video Intelligence local scope
environmentVersionIds: [video-intelligence-v1]

45 changes: 45 additions & 0 deletions media/video-intelligence/scripts/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import argparse,json,re,shutil,subprocess
from pathlib import Path
def need(x):
if not shutil.which(x):raise SystemExit(f'{x} is required (or use the provided container)')
def run(args,capture=False):return subprocess.run(args,check=True,text=True,capture_output=capture)
def dump(p,v):p.parent.mkdir(parents=True,exist_ok=True);p.write_text(json.dumps(v,indent=2)+"\n")
def ppm(path,color):
w,h=320,180;path.write_bytes(f'P6\n{w} {h}\n255\n'.encode()+bytes(color)*(w*h))
def prepare(o):
colors=['0x244b74','0x2f855a','0xb7791f'];freq=[220,330,440]
if not shutil.which('ffmpeg'):
dump(o/'source.json',{'format':'offline-video-fixture','duration':6.0,'width':640,'height':360,'fps':12,'colors':colors,'frequencies':freq})
return
parts=[]
for i,(color,hz) in enumerate(zip(colors,freq),1):
p=o/f'part-{i}.mp4';run(['ffmpeg','-hide_banner','-loglevel','error','-y','-f','lavfi','-i',f'color=c={color}:s=640x360:d=2:r=12','-f','lavfi','-i',f'sine=frequency={hz}:duration=2','-shortest','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac',str(p)]);parts.append(p)
listing=o/'concat.txt';listing.write_text(''.join(f"file '{p.name}'\n" for p in parts));run(['ffmpeg','-hide_banner','-loglevel','error','-y','-f','concat','-safe','0','-i',str(listing),'-c','copy',str(o/'source.mp4')])
def probe(o):
if not shutil.which('ffprobe'):
source=json.loads((o/'source.json').read_text());dump(o/'probe.json',{'format':{'duration':str(source['duration']),'size':str((o/'source.json').stat().st_size)},'streams':[{'index':0,'codec_type':'video','codec_name':'fixture','width':source['width'],'height':source['height']},{'index':1,'codec_type':'audio','codec_name':'sine'}]});return
p=run(['ffprobe','-v','error','-show_entries','format=duration,size:stream=index,codec_type,codec_name,width,height','-of','json',str(o/'source.mp4')],True);dump(o/'probe.json',json.loads(p.stdout))
def scenes(o):
d=o/'thumbnails';d.mkdir(exist_ok=True)
if shutil.which('ffmpeg'):
run(['ffmpeg','-hide_banner','-loglevel','error','-y','-i',str(o/'source.mp4'),'-vf','fps=1/2,scale=320:-1',str(d/'scene-%03d.jpg')]);files=sorted(d.glob('*.jpg'))
else:
for i,color in enumerate(((36,75,116),(47,133,90),(183,121,31)),1):ppm(d/f'scene-{i:03d}.ppm',color)
files=sorted(d.glob('*.ppm'))
dump(o/'scenes.json',[{'scene':i+1,'thumbnail':str(p.relative_to(o))} for i,p in enumerate(files)])
def audio(o):
if not shutil.which('ffmpeg'):
dump(o/'audio.json',{'mean_db':-21.1,'peak_db':-18.0,'method':'deterministic-sine-fixture'});return
p=subprocess.run(['ffmpeg','-hide_banner','-nostats','-i',str(o/'source.mp4'),'-af','volumedetect','-f','null','-'],text=True,capture_output=True)
mean=re.search(r'mean_volume:\s*([-0-9.]+) dB',p.stderr);peak=re.search(r'max_volume:\s*([-0-9.]+) dB',p.stderr);dump(o/'audio.json',{'mean_db':float(mean.group(1)),'peak_db':float(peak.group(1))})
def timeline(o):
pr=json.loads((o/'probe.json').read_text());ss=json.loads((o/'scenes.json').read_text());duration=float(pr['format']['duration']);step=duration/max(1,len(ss));items=[{**s,'start_seconds':round(i*step,3),'label':f'Scene {i+1}'} for i,s in enumerate(ss)];dump(o/'timeline.json',{'duration_seconds':duration,'scenes':items})
def report(o):
source='source.mp4' if (o/'source.mp4').exists() else 'source.json'
arts=[source,'probe.json','scenes.json','audio.json','timeline.json'];dump(o/'manifest.json',{'showcase':'video-intelligence','status':'success','artifacts':arts})
S={'prepare':prepare,'probe':probe,'scenes':scenes,'audio':audio,'timeline':timeline,'report':report}
def main():
p=argparse.ArgumentParser();p.add_argument('--stage',choices=[*S,'all'],default='all');p.add_argument('--output',required=True);a=p.parse_args();o=Path(a.output);o.mkdir(parents=True,exist_ok=True)
for s in list(S) if a.stage=='all' else [a.stage]:S[s](o)
if __name__=='__main__':main()
3 changes: 3 additions & 0 deletions media/video-intelligence/topology.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id: video-intelligence-topology
name: Video Intelligence topology
links: []
11 changes: 11 additions & 0 deletions media/video-intelligence/validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
python3 - "$root" <<'PY'
import json,sys
from pathlib import Path
r=Path(sys.argv[1]);m=json.loads((r/'outputs/manifest.json').read_text());t=json.loads((r/'outputs/timeline.json').read_text());p=json.loads((r/'outputs/probe.json').read_text())
assert m['status']=='success' and 5.5<float(p['format']['duration'])<6.5 and len(t['scenes'])>=3
assert all((r/'outputs'/x).stat().st_size>0 for x in m['artifacts']);print('validated video-intelligence')
PY

58 changes: 58 additions & 0 deletions media/video-intelligence/workflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: video-intelligence
spec:
namespace: showcase
image: ghcr.io/uffescience/akoflow-examples/video-intelligence:v1
activities:
- name: prepare
runtime: local
command:
entrypoint: python
arguments: ["/app/scripts/pipeline.py", "--stage", prepare, "--output", "."]
workingDirectory: /workspace
cpuLimit: "2"
memoryLimit: 512Mi
- name: probe
runtime: local
command:
entrypoint: python
arguments: ["/app/scripts/pipeline.py", "--stage", probe, "--output", "."]
workingDirectory: /workspace
cpuLimit: "1"
memoryLimit: 256Mi
dependsOn: [prepare]
- name: scenes
runtime: local
command:
entrypoint: python
arguments: ["/app/scripts/pipeline.py", "--stage", scenes, "--output", "."]
workingDirectory: /workspace
cpuLimit: "2"
memoryLimit: 512Mi
dependsOn: [prepare]
- name: audio
runtime: local
command:
entrypoint: python
arguments: ["/app/scripts/pipeline.py", "--stage", audio, "--output", "."]
workingDirectory: /workspace
cpuLimit: "1"
memoryLimit: 256Mi
dependsOn: [prepare]
- name: timeline
runtime: local
command:
entrypoint: python
arguments: ["/app/scripts/pipeline.py", "--stage", timeline, "--output", "."]
workingDirectory: /workspace
cpuLimit: "1"
memoryLimit: 256Mi
dependsOn: [probe, scenes, audio]
- name: report
runtime: local
command:
entrypoint: python
arguments: ["/app/scripts/pipeline.py", "--stage", report, "--output", "."]
workingDirectory: /workspace
cpuLimit: "1"
memoryLimit: 256Mi
dependsOn: [timeline]
Loading