How to Build a Jyotish-Powered AI Music Composition Pipeline: Swiss Ephemeris → MIDI → Video

How to Build a Jyotish-Powered AI Music Composition Pipeline: Swiss Ephemeris → MIDI → Video

Apr 21, 2026
⚡ AI Engineering Deep Dive


A hands-on technical walkthrough of the exact stack we built to pull live planetary data from the Swiss Ephemeris, run it through a Jyotish interpretation layer, translate it into MIDI composition parameters, and generate cinematic visual prompt packs — all automated inside OpenClaw.

The video above is the output. This post is the engineering underneath it. We are going to walk through the exact pipeline we built — from astronomical data ingestion all the way to a compressed, share-ready video asset — so you can replicate it, fork it, and build your own version.

This is not a conceptual overview. This is the actual config, the actual scripts, and the actual mistakes we made along the way.

The Full Pipeline at a Glance

Swiss Ephemeris
Sidereal Positions
Jyotish Skill
Music Parameters
MIDI Files
Visual Prompts
Video Assembly
Compressed Output

Each stage is a discrete step. Each one can be swapped, upgraded, or forked independently. That modularity is what makes this a real system rather than a one-off hack.

Part 1 — Pulling Planetary Data From the Swiss Ephemeris

The Swiss Ephemeris is the gold standard for astronomical calculation. It is the same engine used by most serious astrology software. The Python binding pyswisseph gives you programmatic access to sidereal positions for any date and time.

Installation

pip install pyswissephbash

Pulling Today's Sidereal Positions

The key flag is swe.FLG_SIDEREAL combined with the Lahiri ayanamsa, which is the standard reference frame used in Jyotish. Without this flag you get tropical positions — which is fine for Western astrology but wrong for Vedic interpretation.

import swisseph as swe
from datetime import datetime, timezone

# Set Lahiri ayanamsa — required for Jyotish / sidereal positions
swe.set_sid_mode(swe.SIDM_LAHIRI)

def get_sidereal_positions(dt: datetime) -> dict:
    """
    Pull sidereal planetary positions for a given datetime.
    Returns a dict of planet name → (longitude_degrees, sign, degrees_in_sign)
    """
    jd = swe.julday(dt.year, dt.month, dt.day,
                    dt.hour + dt.minute / 60.0)

    planets = {
        "Sun":     swe.SUN,
        "Moon":    swe.MOON,
        "Mercury": swe.MERCURY,
        "Venus":   swe.VENUS,
        "Mars":    swe.MARS,
        "Jupiter": swe.JUPITER,
        "Saturn":  swe.SATURN,
        "Rahu":    swe.MEAN_NODE,   # North node
    }

    SIGNS = [
        "Aries","Taurus","Gemini","Cancer",
        "Leo","Virgo","Libra","Scorpio",
        "Sagittarius","Capricorn","Aquarius","Pisces"
    ]

    results = {}
    for name, planet_id in planets.items():
        pos, _ = swe.calc_ut(jd, planet_id, swe.FLG_SIDEREAL)
        lon = pos[0]
        sign_index = int(lon // 30)
        deg_in_sign = round(lon % 30, 1)
        results[name] = {
            "longitude": round(lon, 4),
            "sign": SIGNS[sign_index],
            "degrees": deg_in_sign
        }

    # Ketu is always exactly opposite Rahu
    rahu_lon = results["Rahu"]["longitude"]
    ketu_lon = (rahu_lon + 180) % 360
    sign_index = int(ketu_lon // 30)
    results["Ketu"] = {
        "longitude": round(ketu_lon, 4),
        "sign": SIGNS[sign_index],
        "degrees": round(ketu_lon % 30, 1)
    }

    return results

# Run it
now = datetime.now(timezone.utc)
positions = get_sidereal_positions(now)

for planet, data in positions.items():
    print(f"{planet:10} {data['sign']:15} {data['degrees']}°")
python

Running this on April 21, 2026 gives you the output we used for this session:

Sun        Aries           5.7°
Moon       Taurus          10.9°
Mercury    Pisces          13.0°
Venus      Taurus          0.7°
Mars       Pisces          13.7°
Jupiter    Gemini          23.3°
Saturn     Pisces          13.7°
Rahu       Aquarius        12.2°
Ketu       Leo             12.2°
output

Why Lahiri? There are several ayanamsa options in pyswisseph. Lahiri is the Indian government standard and is what the vast majority of Jyotish practitioners use. If you are building for a Western astrology use case, skip the swe.set_sid_mode call entirely and use tropical positions.

Part 2 — The Jyotish Interpretation Skill

Raw planetary positions mean nothing to a music composition engine. You need an interpretation layer that converts sign + planet combinations into musical and emotional parameters. This is where the OpenClaw Jyotish skill lives.

Skill File Structure

In your OpenClaw workspace, create the skill file at:

~/.openclaw/agents/main/skills/jyotish-music.mdpath

The Interpretation Config

The skill works as a lookup and synthesis layer. Here is the core mapping structure — the full version of what we run:

# SKILL: Jyotish Music Interpreter
# Purpose: Convert planetary positions into musical composition parameters

## Planet Archetypes

### Sun
- Quality: Solar, assertive, identity, clarity
- Musical role: Lead melodic voice, opening statement
- Default register: Mid-high, bright timbre
- Preferred instruments: Brass, strings (bowed), piano treble

### Moon
- Quality: Emotional, receptive, memory, flow
- Musical role: Harmonic foundation, bass/pad layer
- Default register: Low-mid, warm sustain
- Preferred instruments: Cello, bass, ambient pad, Rhodes

### Mercury
- Quality: Communicative, agile, intellectual, quick
- Musical role: Counter-melody, ornamentation, call-and-response
- Default register: Mid, light articulation
- Preferred instruments: Flute, high strings, marimba, arpeggiated synth

### Venus
- Quality: Beauty, harmony, sensuality, attraction
- Musical role: Harmonic color, chord voicing richness
- Default register: Mid, lush and full
- Preferred instruments: Strings ensemble, Rhodes, acoustic guitar, choir

### Mars
- Quality: Drive, urgency, conflict, courage
- Musical role: Rhythmic engine, tension, syncopation
- Default register: Mid-low, percussive attack
- Preferred instruments: Drums, bass guitar, staccato brass, distorted elements

### Jupiter
- Quality: Expansion, wisdom, generosity, abundance
- Musical role: Secondary melody, harmonic expansion, resolution
- Default register: Mid-high, round and full
- Preferred instruments: French horn, acoustic piano, vocal harmony

### Saturn
- Quality: Discipline, restriction, time, depth
- Musical role: Long held tones, structural anchor, silence
- Default register: Low, sparse, deliberate
- Preferred instruments: Bass clarinet, low strings, deep synth pad, gongs

### Rahu (North Node)
- Quality: Future, obsession, amplification, strangeness
- Musical role: Unusual texture, microtonal element, electronic layer
- Default register: Variable, unexpected
- Preferred instruments: Electronic, processed voice, prepared piano, glitch

### Ketu (South Node)
- Quality: Dissolution, past karma, release, wisdom
- Musical role: Fade elements, reverb tails, resolution passages
- Default register: Fading, sparse, ancient-sounding
- Preferred instruments: Singing bowls, tanpura, fading strings, silence

---

## Sign Modifiers

Apply these modifiers to the planet's base archetype:

| Sign        | Modifier                                      |
|-------------|-----------------------------------------------|
| Aries       | Fast tempo, rising motif, major key tendency  |
| Taurus      | Slow tempo, grounded, rich harmonics, drones  |
| Gemini      | Dual voices, fast runs, light articulation    |
| Cancer      | Emotional depth, minor key, flowing phrases   |
| Leo         | Bold, theatrical, strong downbeat, fanfare    |
| Virgo       | Precise, clean, moderate tempo, detail        |
| Libra       | Balanced, resolved cadences, symmetrical      |
| Scorpio     | Intense, chromatic, descending motifs         |
| Sagittarius | Expansive, adventurous, modal scales          |
| Capricorn   | Austere, slow, structural, disciplined        |
| Aquarius    | Unconventional, electronic, asymmetric        |
| Pisces      | Dreamy, dissolving, suspended chords, reverb  |

---

## Conjunction Rule
When two planets share the same sign within 2 degrees, blend their archetypes.
The closer the conjunction, the more the energies fuse — treat as a single compound voice.
markdown

Part 3 — Translating Jyotish Output Into MIDI Parameters

Once the skill has interpreted the planetary positions, you need to convert the output into actual MIDI parameters. Here is the Python function we use to do that translation:

Planet → MIDI Mapping Table

Planet / Sign MIDI Channel Velocity Range Note Range Duration (ticks)
☉ Sun / Aries Ch 1 80–110 C4–C6 (mid-high) 240–480 (quarter–half)
☽ Moon / Taurus Ch 2 40–70 C2–C4 (low) 480–1920 (half–whole+)
♀ Venus / Taurus Ch 3 55–80 C3–C5 (mid) 480–960 (half–whole)
☿ Mercury / Pisces Ch 4 50–85 C4–C7 (high) 60–240 (16th–quarter)
♂ Mars / Pisces Ch 5 85–120 C2–C4 (low-mid) 120–240 (8th–quarter)
♃ Jupiter / Gemini Ch 6 65–90 C3–C5 (mid) 240–960 (quarter–whole)
♄ Saturn / Pisces Ch 7 30–60 C1–C3 (very low) 960–3840 (whole–breve)
☊ Rahu / Aquarius Ch 8 20–100 (random) Any (unpredictable) Variable, irregular

MIDI File Generation Script

from midiutil import MIDIFile
import random

def build_planetary_midi(positions: dict, output_path: str, bpm: int = 72):
    """
    Takes Swiss Ephemeris sidereal positions and generates a
    multi-track MIDI file with one track per planet.
    """

    # Sign → scale modifier mapping
    SIGN_SCALES = {
        "Aries":       [0, 2, 4, 5, 7, 9, 11],  # Major
        "Taurus":      [0, 2, 3, 5, 7, 8, 10],  # Natural minor
        "Gemini":      [0, 2, 4, 6, 7, 9, 11],  # Lydian
        "Cancer":      [0, 2, 3, 5, 7, 8, 11],  # Harmonic minor
        "Leo":         [0, 2, 4, 5, 7, 9, 11],  # Major (bright)
        "Virgo":       [0, 2, 3, 5, 7, 9, 10],  # Dorian
        "Libra":       [0, 2, 4, 5, 7, 9, 10],  # Mixolydian
        "Scorpio":     [0, 1, 4, 5, 7, 8, 11],  # Double harmonic
        "Sagittarius": [0, 2, 4, 7, 9],          # Pentatonic major
        "Capricorn":   [0, 2, 3, 5, 7, 8, 10],  # Phrygian
        "Aquarius":    [0, 2, 3, 6, 7, 8, 11],  # Enigmatic
        "Pisces":      [0, 2, 3, 5, 7, 8, 10],  # Minor (dreamy)
    }

    # Planet → (channel, base_note, velocity_range, duration_ticks)
    PLANET_CONFIG = {
        "Sun":     (0,  60, (80, 110), (2, 4)),
        "Moon":    (1,  36, (40,  70), (4, 8)),
        "Venus":   (2,  48, (55,  80), (4, 6)),
        "Mercury": (3,  72, (50,  85), (0.5, 1)),
        "Mars":    (4,  40, (85, 120), (1, 2)),
        "Jupiter": (5,  52, (65,  90), (2, 6)),
        "Saturn":  (6,  28, (30,  60), (8, 16)),
        "Rahu":    (7,  random.randint(24, 96), (20, 100), (1, 8)),
    }

    num_tracks = len(PLANET_CONFIG)
    midi = MIDIFile(num_tracks)

    for planet, (channel, base_note, vel_range, dur_range) in PLANET_CONFIG.items():
        if planet not in positions:
            continue

        sign = positions[planet]["sign"]
        degrees = positions[planet]["degrees"]
        scale = SIGN_SCALES.get(sign, [0, 2, 4, 5, 7, 9, 11])

        # Use degree position to seed note selection within scale
        degree_factor = degrees / 30.0  # 0.0 to 1.0

        midi.addTrackName(channel, 0, f"{planet} in {sign}")
        midi.addTempo(channel, 0, bpm)

        time = 0
        total_beats = 32  # 8 bars of 4/4

        while time < total_beats:
            # Select note from scale based on degree position
            scale_degree = int(degree_factor * len(scale))
            note = base_note + scale[scale_degree % len(scale)]
            note = max(0, min(127, note))  # MIDI note bounds

            velocity = random.randint(*vel_range)
            duration = random.uniform(*dur_range)

            midi.addNote(channel, channel, note, time, duration, velocity)

            # Advance time — Saturn holds long, Mercury skips fast
            time += duration + random.uniform(0, duration * 0.5)

    with open(output_path, "wb") as f:
        midi.writeFile(f)

    print(f"MIDI written → {output_path}")

# Run it
build_planetary_midi(
    positions=positions,
    output_path="~/openclaw_workspace/media/jyotish_2026_04_21.mid",
    bpm=68  # Slower tempo — Moon + Venus in Taurus governs the feel
)
python

Dependency: This uses midiutil. Install with pip install MIDIUtil. For more control over CC messages, pitch bend, and instrument assignment, switch to pretty_midi or music21.

Part 4 — Generating Visual Prompt Packs From Planetary Data

The same planetary reading that drives the music also drives the visual world. We run a second prompt generation pass that converts each planet's sign placement into a scene description — ready to drop into Midjourney, Flux, or any image generator.

The Prompt Generation Skill

Add this section to your Jyotish skill file:

## Visual Scene Generation Rules

For each planet, generate ONE scene description.
The scene should:
- Reflect the planet's archetype AND the sign's elemental quality
- Be photorealistic and cinematic, not illustrated or cartoon
- Be 3–5 sentences max — specific, imagistic, atmospheric
- Include: lighting quality, color palette, mood, time of day, primary subject

### Element → Visual Environment Mapping
- Fire signs (Aries, Leo, Sagittarius): golden hour, open landscapes, sunlight
- Earth signs (Taurus, Virgo, Capricorn): natural textures, low light, grounded
- Air signs (Gemini, Libra, Aquarius): open sky, movement, atmospheric perspective
- Water signs (Cancer, Scorpio, Pisces): water surfaces, mist, dissolving edges

### Planet → Subject Archetype
- Sun: a single figure in open light, facing forward
- Moon: a woman near water, facing away, moonlit
- Venus: soft beauty — flowers, fabric, warmth
- Mercury: movement, birds, wind, quick gesture
- Mars: tension, fire, a held breath before action
- Jupiter: expansive landscape, abundance, open sky
- Saturn: ancient stone, long shadows, deep time
- Rahu: something strange at the edge of the frame
- Ketu: an absence, a doorway, something leaving
markdown

Running the Prompt Pack Generation

Inside OpenClaw, trigger the skill with this prompt:

/skill jyotish-music

Using today's planetary positions:
Sun in Aries 5.7°
Moon in Taurus 10.9°
Venus in Taurus 0.7°
Mercury in Pisces 13.0°
Mars in Pisces 13.7°
Saturn in Pisces 13.7°
Jupiter in Gemini 23.3°
Rahu in Aquarius 12.2°
Ketu in Leo 12.2°

Generate:
1. Music parameters for each planet (key, tempo, texture, instrument)
2. A visual scene description for each planet
3. A unified song title and emotional theme for today's session
4. Recommended BPM and root key for the full composition

Output as structured JSON.
openclaw

Part 5 — Video Assembly and Compression

Once you have your generated clips from your image-to-video tool of choice (Runway, Kling, Sora, Luma), the assembly and compression step is handled by ffmpeg. Do not skip the compression step — raw video exports will choke any upload pipeline.

Assemble Clips Into Final Video

# Create a clips manifest file
cat > clips.txt << EOF
file '/home/user/openclaw_workspace/media/clip_moon_taurus.mp4'
file '/home/user/openclaw_workspace/media/clip_sun_aries.mp4'
file '/home/user/openclaw_workspace/media/clip_mars_saturn_pisces.mp4'
file '/home/user/openclaw_workspace/media/clip_jupiter_gemini.mp4'
file '/home/user/openclaw_workspace/media/clip_rahu_aquarius.mp4'
EOF

# Concatenate clips
ffmpeg -f concat -safe 0 -i clips.txt -c copy raw_assembly.mp4
bash

Compress for Social Delivery

# Compress to H.264 — safe for Telegram, YouTube, Instagram
ffmpeg -i raw_assembly.mp4 \
  -vcodec libx264 \
  -acodec aac \
  -crf 23 \
  -preset slow \
  -movflags +faststart \
  -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" \
  share_ready.mp4
bash

Critical lesson learned: Never pass a raw .mov file directly to your OpenClaw agent for upload. Always compress to H.264 first. A raw .mov can be 2–8GB. Telegram's bot API hard caps at 50MB. The compression step above typically gets a 3-minute 1080p video under 80MB with no visible quality loss.

Upload to Telegram via Script

import requests
import os

BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
CHAT_ID   = os.environ["TELEGRAM_CHAT_ID"]
VIDEO_PATH = os.path.expanduser(
    "~/openclaw_workspace/media/share_ready.mp4"
)

def upload_video(path: str, caption: str = ""):
    url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendVideo"
    with open(path, "rb") as f:
        resp = requests.post(url, data={
            "chat_id": CHAT_ID,
            "caption": caption,
            "supports_streaming": True
        }, files={"video": f}, timeout=120)
    return resp.json()

result = upload_video(
    VIDEO_PATH,
    caption="🎵 Jyotish Music Session — April 21, 2026\nMoon + Venus in Taurus · Mars + Saturn in Pisces"
)
print(result)
python

Part 6 — Automating the Whole Pipeline as an OpenClaw Cron Job

Running this manually is fine for experimentation. The real power comes when you set this up as a scheduled cron job that runs every morning, pulls fresh planetary data, and drops a new music session output into your Content Office automatically.

Cron Job Config in OpenClaw

crons:
  - id: jyotish-music-daily
    schedule: "0 6 * * *"        # 6:00 AM daily
    agent: main
    task: |
      Run the daily Jyotish music session.

      Steps:
      1. Call the Swiss Ephemeris Python script to get today's sidereal positions
      2. Pass the positions through the jyotish-music skill
      3. Generate MIDI parameters and write the MIDI file to workspace/media/
      4. Generate the visual prompt pack for today's planetary configuration
      5. Save all outputs to workspace/media/jyotish_YYYY_MM_DD/
      6. Write a session summary to workspace/reports/jyotish_daily.md
      7. Post the summary to Telegram group

    tools:
      - exec
      - fs.write
      - telegram.send
yaml

Add this to your config at: ~/.openclaw/agents/main/crons.yaml — then run openclaw gateway restart to activate it. Verify it registered with openclaw status — you should see the cron listed under Tasks.

"The pipeline runs whether you are at your desk or not. That is the point."

Mistakes We Made — So You Don't Have To

  • Forgetting swe.set_sid_mode(). Without this you get tropical positions. The music will still work but the Jyotish interpretation will be wrong. Always set Lahiri before calling calc_ut.
  • Passing .mov files directly to the agent. The cron job froze three sessions trying to handle a raw video file. Always compress to H.264 before any agent touches the file.
  • Not setting --movflags +faststart. Without this flag, the video has to fully download before playback starts. On Telegram this causes a long delay. The flag moves the metadata to the front of the file so it streams immediately.
  • Using the wrong Telegram ID. Your personal user ID and your group chat ID are different. The group ID starts with a negative number (-521...). Use getUpdates on your bot to confirm the right ID before wiring it into the script.
  • Running duplicate cron sessions. If the gateway restarts mid-cron, you can end up with zombie sessions. Run openclaw sessions cleanup followed by a gateway restart to clear them.

Join the Tech Temple Alpha Group

Daily alpha reports, live pipeline builds, and the real technical conversations behind everything we publish. Free to join — not free to ignore.

Join Tech Temple on Telegram →

Keywords: Swiss Ephemeris Python, pyswisseph tutorial, Jyotish MIDI composition, sidereal astrology code, planetary music generation, MIDI from astrology, AI music pipeline, OpenClaw cron job, ffmpeg video compression, Telegram bot video upload, Jyotish skill OpenClaw, planetary data music, AI creative automation, music composition AI, Chief Wizard, Determination Development, AI engineering, agentic creative workflow, midiutil Python, Swiss Ephemeris Lahiri

What Do You Think?

Drop a comment below — questions, pushback, or your own take. This is where the real conversation happens.

JT
James Tipton

James Tipton is the creator of Determination Development, empowering creators with new technology workflows.

CW
Chief Wizard

Chief Wizard is the custom AI James built to deliver deep research, strategic insight, and transformational transmissions in service of human growth.