CI/CD Examples

Ready-to-use examples for integrating LogTalk into your release workflow. Automatically generate audio or video content whenever you publish a new release.

GitHub Actions

Trigger audio generation on release publish. This workflow extracts the release notes and creates an audio version automatically.

.github/workflows/release-audio.yml
name: Generate Release Audio
on:
release:
types: [published]
jobs:
generate-audio:
runs-on: ubuntu-latest
steps:
- name: Generate changelog audio
env:
LOGTALK_API_KEY: ${{ secrets.LOGTALK_API_KEY }}
run: |
# Extract release body
CHANGELOG="${{ github.event.release.body }}"
VERSION="${{ github.event.release.tag_name }}"
# Create episode
RESPONSE=$(curl -s -X POST https://api.logtalk.io/v1/episodes \
-H "Authorization: Bearer $LOGTALK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ${{ github.repository }}-$VERSION" \
-d "{
\"content\": $(echo "$CHANGELOG" | jq -Rs .),
\"mode\": \"changelog\",
\"output_format\": \"audio\",
\"settings\": {
\"product_name\": \"${{ github.repository }}\",
\"version\": \"$VERSION\",
\"tone\": \"casual\"
}
}")
# Check for success
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" != "true" ]; then
echo "Error: $(echo "$RESPONSE" | jq -r '.error.message')"
exit 1
fi
# Output audio URL
AUDIO_URL=$(echo "$RESPONSE" | jq -r '.data.audio_url')
echo "Audio generated: $AUDIO_URL"
echo "AUDIO_URL=$AUDIO_URL" >> $GITHUB_OUTPUT
- name: Update release with audio link
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release edit ${{ github.event.release.tag_name }} \
--notes "${{ github.event.release.body }}
---
**Listen to this release:** [Audio version]($AUDIO_URL)"

Advanced: Video GenerationComing Soon

For video generation with async processing and webhook notification:

.github/workflows/release-video.yml
name: Generate Release Video
on:
release:
types: [published]
jobs:
generate-video:
runs-on: ubuntu-latest
steps:
- name: Start video generation
id: create
env:
LOGTALK_API_KEY: ${{ secrets.LOGTALK_API_KEY }}
run: |
CHANGELOG="${{ github.event.release.body }}"
VERSION="${{ github.event.release.tag_name }}"
RESPONSE=$(curl -s -X POST https://api.logtalk.io/v1/episodes \
-H "Authorization: Bearer $LOGTALK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: video-${{ github.repository }}-$VERSION" \
-d "{
\"content\": $(echo "$CHANGELOG" | jq -Rs .),
\"mode\": \"changelog\",
\"output_format\": \"video\",
\"wait\": true,
\"settings\": {
\"product_name\": \"${{ github.repository }}\",
\"version\": \"$VERSION\"
}
}")
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" != "true" ]; then
echo "Error: $(echo "$RESPONSE" | jq -r '.error.message')"
exit 1
fi
VIDEO_URL=$(echo "$RESPONSE" | jq -r '.data.video_url')
echo "video_url=$VIDEO_URL" >> $GITHUB_OUTPUT
- name: Post video to Slack
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
curl -X POST $SLACK_WEBHOOK \
-H "Content-Type: application/json" \
-d "{
\"text\": \"New release video for ${{ github.event.release.tag_name }}\",
\"attachments\": [{
\"title\": \"Watch Release Video\",
\"title_link\": \"${{ steps.create.outputs.video_url }}\"
}]
}"

GitLab CI/CD

Generate audio from your CHANGELOG.md when pushing tags:

.gitlab-ci.yml
stages:
- release
generate_changelog_audio:
stage: release
image: curlimages/curl:latest
rules:
- if: $CI_COMMIT_TAG
variables:
LOGTALK_API_KEY: $LOGTALK_API_KEY
script:
- |
# Extract changelog for this version
VERSION=$CI_COMMIT_TAG
# Read CHANGELOG.md content (you may want to extract just the relevant section)
CHANGELOG=$(cat CHANGELOG.md | head -100)
# Generate audio
RESPONSE=$(curl -s -X POST https://api.logtalk.io/v1/episodes \
-H "Authorization: Bearer $LOGTALK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $CI_PROJECT_PATH-$VERSION" \
-d "{
\"content\": $(echo "$CHANGELOG" | jq -Rs .),
\"output_format\": \"audio\",
\"settings\": {
\"product_name\": \"$CI_PROJECT_NAME\",
\"version\": \"$VERSION\"
}
}")
# Parse response
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" = "true" ]; then
AUDIO_URL=$(echo "$RESPONSE" | jq -r '.data.audio_url')
echo "Audio URL: $AUDIO_URL"
else
echo "Error: $(echo "$RESPONSE" | jq -r '.error.message')"
exit 1
fi

Bash Script

A reusable bash script for converting changelogs from the command line:

logtalk-convert.sh
#!/bin/bash
# logtalk-convert.sh - Convert changelog to audio
set -e
# Configuration
API_KEY="${LOGTALK_API_KEY:?LOGTALK_API_KEY environment variable not set}"
API_URL="https://api.logtalk.io/v1/episodes"
# Parse arguments
CHANGELOG_FILE=""
OUTPUT_FORMAT="audio"
PRODUCT_NAME=""
VERSION=""
usage() {
echo "Usage: $0 -f <changelog_file> [-o audio|video] [-p product_name] [-v version]"
echo ""
echo "Options:"
echo " -f Path to changelog file (required)"
echo " -o Output format: audio (default) or video"
echo " -p Product name"
echo " -v Version number"
exit 1
}
while getopts "f:o:p:v:h" opt; do
case $opt in
f) CHANGELOG_FILE="$OPTARG" ;;
o) OUTPUT_FORMAT="$OPTARG" ;;
p) PRODUCT_NAME="$OPTARG" ;;
v) VERSION="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
if [ -z "$CHANGELOG_FILE" ]; then
echo "Error: Changelog file is required"
usage
fi
if [ ! -f "$CHANGELOG_FILE" ]; then
echo "Error: File not found: $CHANGELOG_FILE"
exit 1
fi
# Read changelog content
CONTENT=$(cat "$CHANGELOG_FILE")
# Build settings JSON
SETTINGS="{}"
if [ -n "$PRODUCT_NAME" ]; then
SETTINGS=$(echo "$SETTINGS" | jq --arg name "$PRODUCT_NAME" '.product_name = $name')
fi
if [ -n "$VERSION" ]; then
SETTINGS=$(echo "$SETTINGS" | jq --arg ver "$VERSION" '.version = $ver')
fi
# Make API request
echo "Converting changelog to $OUTPUT_FORMAT..."
RESPONSE=$(curl -s -X POST "$API_URL" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"content\": $(echo "$CONTENT" | jq -Rs .),
\"output_format\": \"$OUTPUT_FORMAT\",
\"settings\": $SETTINGS
}")
# Check result
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" = "true" ]; then
STATUS=$(echo "$RESPONSE" | jq -r '.data.status')
if [ "$STATUS" = "completed" ]; then
AUDIO_URL=$(echo "$RESPONSE" | jq -r '.data.audio_url')
DURATION=$(echo "$RESPONSE" | jq -r '.data.duration_seconds')
echo "Success!"
echo "Duration: ${DURATION}s"
echo "Audio URL: $AUDIO_URL"
elif [ "$STATUS" = "processing" ]; then
POLL_URL=$(echo "$RESPONSE" | jq -r '.data.poll_url')
echo "Processing started. Poll for status at:"
echo "$POLL_URL"
fi
else
ERROR=$(echo "$RESPONSE" | jq -r '.error.message')
echo "Error: $ERROR"
exit 1
fi

Usage:

# Basic usage
./logtalk-convert.sh -f CHANGELOG.md
# With options
./logtalk-convert.sh -f CHANGELOG.md -p "MyApp" -v "2.0.0"
# Video output (coming soon)
# ./logtalk-convert.sh -f CHANGELOG.md -p "MyApp" -v "2.0.0" -o video
# Make sure to set your API key
export LOGTALK_API_KEY="lt_live_your_key_here"

Node.js

A complete Node.js module for interacting with the LogTalk API:

logtalk.js
// logtalk.js - LogTalk API client
const LOGTALK_API_KEY = process.env.LOGTALK_API_KEY;
const BASE_URL = 'https://api.logtalk.io/v1';
/**
* Create a episode
*/
async function createConversion(options) {
const {
content,
mode = 'changelog',
outputFormat = 'audio',
settings = {},
webhookUrl,
idempotencyKey,
} = options;
const headers = {
'Authorization': `Bearer ${LOGTALK_API_KEY}`,
'Content-Type': 'application/json',
};
if (idempotencyKey) {
headers['Idempotency-Key'] = idempotencyKey;
}
const response = await fetch(`${BASE_URL}/episodes`, {
method: 'POST',
headers,
body: JSON.stringify({
content,
mode,
output_format: outputFormat,
settings,
webhook_url: webhookUrl,
}),
});
const data = await response.json();
if (!data.success) {
throw new Error(`LogTalk API Error: ${data.error.message}`);
}
return data.data;
}
/**
* Get episode status
*/
async function getConversion(episodeId) {
const response = await fetch(`${BASE_URL}/episodes/${episodeId}`, {
headers: {
'Authorization': `Bearer ${LOGTALK_API_KEY}`,
},
});
const data = await response.json();
if (!data.success) {
throw new Error(`LogTalk API Error: ${data.error.message}`);
}
return data.data;
}
/**
* Wait for episode to complete (polling)
*/
async function waitForConversion(episodeId, maxWaitMs = 120000) {
const startTime = Date.now();
const pollInterval = 5000;
while (Date.now() - startTime < maxWaitMs) {
const episode = await getConversion(episodeId);
if (episode.status === 'completed') {
return episode;
}
if (episode.status === 'failed') {
throw new Error(`Conversion failed: ${episode.error?.message}`);
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error('Conversion timed out');
}
/**
* Get current usage
*/
async function getUsage() {
const response = await fetch(`${BASE_URL}/usage`, {
headers: {
'Authorization': `Bearer ${LOGTALK_API_KEY}`,
},
});
const data = await response.json();
if (!data.success) {
throw new Error(`LogTalk API Error: ${data.error.message}`);
}
return data.data;
}
module.exports = {
createConversion,
getConversion,
waitForConversion,
getUsage,
};

Usage in your application:

const logtalk = require('./logtalk');
async function generateReleaseAudio(version, changelog) {
try {
// Check usage first
const usage = await logtalk.getUsage();
console.log(`Audio quota: ${usage.audio.remaining}/${usage.audio.quota} remaining`);
if (usage.audio.remaining === 0) {
throw new Error('No audio credits remaining');
}
// Create episode
const episode = await logtalk.createConversion({
content: changelog,
settings: {
product_name: 'MyApp',
version: version,
tone: 'casual',
},
idempotencyKey: `release-${version}`,
});
console.log(`Audio generated: ${episode.audio_url}`);
return episode;
} catch (error) {
console.error('Failed to generate audio:', error.message);
throw error;
}
}
// Example usage
generateReleaseAudio('2.0.0', '## v2.0.0\n- New feature\n- Bug fix');

Python

A Python module for the LogTalk API:

logtalk.py
# logtalk.py - LogTalk API client
import os
import time
import requests
LOGTALK_API_KEY = os.environ.get('LOGTALK_API_KEY')
BASE_URL = 'https://api.logtalk.io/v1'
class LogTalkError(Exception):
"""LogTalk API error"""
def __init__(self, code, message):
self.code = code
self.message = message
super().__init__(f"{code}: {message}")
def _make_request(method, endpoint, **kwargs):
"""Make an authenticated API request"""
headers = kwargs.pop('headers', {})
headers['Authorization'] = f'Bearer {LOGTALK_API_KEY}'
response = requests.request(
method,
f'{BASE_URL}{endpoint}',
headers=headers,
**kwargs
)
data = response.json()
if not data.get('success'):
error = data.get('error', {})
raise LogTalkError(error.get('code'), error.get('message'))
return data.get('data')
def create_episode(
content: str,
mode: str = 'changelog',
output_format: str = 'audio',
settings: dict = None,
webhook_url: str = None,
idempotency_key: str = None,
):
"""Create a new episode"""
headers = {}
if idempotency_key:
headers['Idempotency-Key'] = idempotency_key
payload = {
'content': content,
'mode': mode,
'output_format': output_format,
}
if settings:
payload['settings'] = settings
if webhook_url:
payload['webhook_url'] = webhook_url
return _make_request('POST', '/episodes', json=payload, headers=headers)
def get_episode(episode_id: str):
"""Get episode status"""
return _make_request('GET', f'/episodes/{episode_id}')
def wait_for_episode(episode_id: str, max_wait_seconds: int = 120):
"""Poll until episode completes"""
start_time = time.time()
poll_interval = 5
while time.time() - start_time < max_wait_seconds:
episode = get_episode(episode_id)
if episode['status'] == 'completed':
return episode
if episode['status'] == 'failed':
error = episode.get('error', {})
raise LogTalkError(
error.get('code', 'GENERATION_FAILED'),
error.get('message', 'Conversion failed')
)
time.sleep(poll_interval)
raise LogTalkError('TIMEOUT', 'Conversion timed out')
def get_usage():
"""Get current usage statistics"""
return _make_request('GET', '/usage')
# Example usage
if __name__ == '__main__':
# Check usage
usage = get_usage()
print(f"Audio: {usage['audio']['remaining']}/{usage['audio']['quota']} remaining")
# Create episode
changelog = """
## v2.0.0
### Added
- Dark mode support
- Real-time collaboration
### Fixed
- Login timeout issue
"""
result = create_episode(
content=changelog,
settings={
'product_name': 'MyApp',
'version': '2.0.0',
'tone': 'casual'
}
)
print(f"Audio URL: {result['audio_url']}")