You sit down for a family movie. The popcorn is ready. Two characters start looking at each other with suspiciously good lighting.
Suddenly, you’re reaching for the remote like you’re defusing a bomb.
Naturally, the engineering response is to build an application that removes kissing, sex, and intimate scenes. Your children can continue believing babies arrive through a logistics provider with excellent last-mile delivery.
The interesting part is that we can search a video using an ordinary description, like “two people kissing,” without training a dedicated kissing detector.
That’s called open-vocabulary search. Your search terms don’t have to come from a fixed menu of labels.
1. Give the movie something searchable
We’ll use jinaai/jina-embeddings-v5-omni-nano, the multimodal member of Jina’s v5 family. It represents text and video in a shared vector space, letting us compare a written description with a clip. Use the Omni model here; the text-only version cannot watch your movie. Jina’s model card
The workflow is small:
- Divide the timeline into short windows.
- Embed sampled frames from each window.
- Compare them with descriptions of the scenes we want to find.
- Review matching timestamps.
- Export a copy with selected intervals removed.
These are time windows, not detected cinematic scenes. We’ll start with two seconds per window and four sampled frames per second. Brief events can still slip between samples, so this is a prototype, not a parental guarantee.
2. Install the pieces
Start with a short local MP4 before processing an entire film.
pip install "torch>=2.5" "transformers>=5,<6" \
sentence-transformers pillow numpy \
"moviepy>=2,<3" imageio-ffmpeg
A CUDA GPU is useful for testing. If you don’t have one, you can use a Runpod GPU Pod with its PyTorch template. This post hasn’t been sponsored by them. Apparently, protecting fictional children from fictional romance does not qualify for a marketing budget.
The model requires trust_remote_code=True, which runs code from its repository. Review that code and pin a reviewed revision when turning this into a maintained application.
3. Find the suspicious affection
We’ll search with several descriptions. A kissing query alone may miss a sex scene without kissing, while a broad query about intimacy might enthusiastically flag an innocent hug.
Save this as find_scenes.py, alongside movie.mp4:
import csv
import numpy as np
from moviepy import VideoFileClip
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(
"jinaai/jina-embeddings-v5-omni-nano",
trust_remote_code=True,
device="cuda",
model_kwargs={
"default_task": "retrieval",
"modality": "vision",
},
)
descriptions = [
"Two people kissing on the lips",
"A sex scene between adults",
"Adults undressing each other romantically",
"Adults touching and embracing intimately in bed",
]
queries = model.encode_query(
descriptions,
normalize_embeddings=True,
convert_to_numpy=True,
)
rows = []
vectors = []
with VideoFileClip("movie.mp4", audio=False) as video:
preview = video.resized(width=384)
for start in np.arange(0, video.duration, 2.0):
end = min(start + 2.0, video.duration)
frames = np.stack([
preview.get_frame(float(t))
for t in np.arange(start, end, 0.25)
]).astype(np.uint8)
vector = model.encode_document(
[frames],
normalize_embeddings=True,
convert_to_numpy=True,
)[0]
scores = queries @ vector
best = int(np.argmax(scores))
vectors.append(vector)
rows.append({
"start": float(start),
"end": float(end),
"score": float(scores[best]),
"match": descriptions[best],
"remove": "no",
})
# Preserve chronological embeddings for future searches.
np.save("embeddings.npy", np.stack(vectors))
with open("windows.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
with open("review.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(
sorted(rows, key=lambda row: row["score"], reverse=True)
)
Jina documents both in-memory video frames and separate encode_query() / encode_document() methods for retrieval. Normalizing the vectors makes their dot product a cosine similarity score. Input formats and retrieval usage
Run:
python find_scenes.py
Open review.csv. Each row includes its strongest matching description. Watch the highest-ranked intervals, change remove to yes for the ones you want cut, and adjust their start and end times as needed. Timestamps are in seconds.
A similarity score is not a probability that a scene contains kissing or sex. Close conversation might rank highly. An actual intimate scene might rank lower. There’s no universal threshold where responsible parenting begins.
The match column is also just the closest search description, not a confirmed label. Every window gets a match, including the establishing shot of a completely innocent mountain.
You can reuse the saved embeddings for different descriptions without processing the movie again.
4. Perform the extremely modest director’s cut
Save this as cut_scenes.py:
import csv
import math
from moviepy import VideoFileClip, concatenate_videoclips
with open("review.csv", newline="") as f:
cuts = sorted(
(float(r["start"]), float(r["end"]))
for r in csv.DictReader(f)
if r["remove"].strip().lower() == "yes"
)
with VideoFileClip("movie.mp4") as video:
merged = []
for start, end in cuts:
if not (
math.isfinite(start)
and math.isfinite(end)
and 0 <= start < end <= video.duration
):
raise ValueError(f"Invalid interval: {start}, {end}")
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
keep = []
cursor = 0.0
for start, end in merged:
if start > cursor:
keep.append(video.subclipped(cursor, start))
cursor = end
if cursor < video.duration:
keep.append(video.subclipped(cursor, video.duration))
if not keep:
raise ValueError("Every frame was selected. Bold edit.")
with concatenate_videoclips(keep) as result:
result.write_videofile(
"movie_edited.mp4",
codec="libx264",
audio_codec="aac",
)
Then:
python cut_scenes.py
This merges overlapping cuts and joins the remaining video with its corresponding audio. MoviePy re-encodes the result; the source file stays intact. This minimal export uses the loaded video and audio, and does not preserve every subtitle or alternate audio track. MoviePy’s editing documentation
The model calls follow Jina’s documented interface; detection accuracy has not been tested here on a movie.
Intimacy is annoyingly contextual
A kiss, a hug, and a sex scene are different things. Even “intimate” can describe anything from a quiet conversation to a scene that sends you searching for the remote under three cushions.
Use descriptions that match what you actually want removed. Review a few seconds before and after each result, then extend the cut to cover the full moment. Otherwise, you might remove the kiss while leaving the entire buildup and a rather confusing aftermath.
There’s another limitation: Jina’s video input reads frames, not the soundtrack automatically. Suggestive dialogue or off-screen sexual activity may require separate audio analysis or timestamped transcription. The example here searches visible content. Jina’s video-input documentation
For better coverage, try overlapping windows and denser frame sampling, then measure what the system misses on clips you have reviewed yourself. More sampling costs more processing. It also remains cheaper than discovering your filter’s limitations during family movie night.
When the family library becomes infrastructure
For one movie, a NumPy array is enough. For many videos, I’d store vectors with video_id, start time, end time, and model version in Qdrant or Elasticsearch. That gives us indexed similarity search and metadata filtering across the collection.
Before making this a commercial application, note that the model’s published license is CC BY-NC 4.0; Jina directs commercial users to contact them. Model license
The useful capability is simple: describe a moment, retrieve likely matches, and turn timestamps into edits. Your search vocabulary can change without training another detector. Deciding what belongs in the final cut is still your job.
Would you like me to cover the version that handles many videos with Qdrant or Elasticsearch? Say so in the comments.
Top comments (0)