feat: Import 35+ skills, merge duplicates, add openclaw installer
Major updates: - Added 35+ new skills from awesome-opencode-skills and antigravity repos - Merged SEO skills into seo-master - Merged architecture skills into architecture - Merged security skills into security-auditor and security-coder - Merged testing skills into testing-master and testing-patterns - Merged pentesting skills into pentesting - Renamed website-creator to thai-frontend-dev - Replaced skill-creator with github version - Removed Chutes references (use MiniMax API instead) - Added install-openclaw-skills.sh for cross-platform installation - Updated .env.example with MiniMax API credentials
This commit is contained in:
329
skills/minimax-multimodal-toolkit/scripts/video/generate_video.sh
Executable file
329
skills/minimax-multimodal-toolkit/scripts/video/generate_video.sh
Executable file
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env bash
|
||||
# MiniMax Video Generation CLI (pure bash)
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/video/generate_video.sh --mode t2v --prompt "A cat playing piano" -o output/cat.mp4
|
||||
# bash scripts/video/generate_video.sh --mode i2v --prompt "Gentle breeze" --first-frame image.jpg -o output/anim.mp4
|
||||
# bash scripts/video/generate_video.sh --mode sef --first-frame start.jpg --last-frame end.jpg -o output/sef.mp4
|
||||
# bash scripts/video/generate_video.sh --mode ref --prompt "Person dancing" --subject-image person.jpg -o output/ref.mp4
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
API_BASE="${MINIMAX_API_HOST:-https://api.minimaxi.com}/v1"
|
||||
POLL_INTERVAL=10
|
||||
MAX_WAIT_TIME=600
|
||||
REQUEST_TIMEOUT=60
|
||||
MAX_CONSECUTIVE_FAILURES=5
|
||||
|
||||
# ============================================================================
|
||||
# Common functions
|
||||
# ============================================================================
|
||||
|
||||
load_env() {
|
||||
local env_file
|
||||
for env_file in "$PROJECT_ROOT/.env" "$(pwd)/.env"; do
|
||||
if [[ -f "$env_file" ]]; then
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%%#*}"; line="$(echo "$line" | xargs)"
|
||||
[[ -z "$line" || "$line" != *=* ]] && continue
|
||||
local key="${line%%=*}" val="${line#*=}"
|
||||
key="$(echo "$key" | xargs)"; val="$(echo "$val" | xargs)"
|
||||
if [[ ${#val} -ge 2 ]]; then
|
||||
case "$val" in \"*\") val="${val:1:${#val}-2}" ;; \'*\') val="${val:1:${#val}-2}" ;; esac
|
||||
fi
|
||||
[[ -z "${!key:-}" ]] && export "$key=$val"
|
||||
done < "$env_file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_api_key() {
|
||||
if [[ -z "${MINIMAX_API_KEY:-}" ]]; then
|
||||
echo "Error: MINIMAX_API_KEY environment variable is not set." >&2; exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
image_to_data_url() {
|
||||
local path="$1"
|
||||
[[ -f "$path" ]] || { echo "Error: Image not found: $path" >&2; exit 1; }
|
||||
local mime
|
||||
mime="$(file -b --mime-type "$path" 2>/dev/null)" || mime="image/jpeg"
|
||||
local b64
|
||||
b64="$(base64 < "$path")"
|
||||
echo "data:${mime};base64,${b64}"
|
||||
}
|
||||
|
||||
resolve_image() {
|
||||
local input="$1"
|
||||
[[ -z "$input" ]] && return
|
||||
case "$input" in
|
||||
http://*|https://*|data:*) echo "$input" ;;
|
||||
*) image_to_data_url "$input" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Video generation functions
|
||||
# ============================================================================
|
||||
|
||||
create_task() {
|
||||
local payload="$1"
|
||||
echo "Creating video generation task..." >&2
|
||||
local raw_output http_code response
|
||||
raw_output="$(curl -s -w "\n%{http_code}" \
|
||||
-X POST "${API_BASE}/video_generation" \
|
||||
-H "Authorization: Bearer ${MINIMAX_API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--max-time "$REQUEST_TIMEOUT" \
|
||||
-d "$payload")"
|
||||
http_code="${raw_output##*$'\n'}"
|
||||
response="${raw_output%$'\n'*}"
|
||||
|
||||
if [[ "$http_code" -ge 400 ]] 2>/dev/null; then
|
||||
echo "Error: API returned HTTP $http_code" >&2; echo "$response" >&2; exit 1
|
||||
fi
|
||||
|
||||
local sc
|
||||
sc="$(echo "$response" | jq -r '.base_resp.status_code // 0')" 2>/dev/null || true
|
||||
if [[ "$sc" != "0" && -n "$sc" ]]; then
|
||||
echo "Error: API error: $(echo "$response" | jq '.base_resp')" >&2; exit 1
|
||||
fi
|
||||
|
||||
local task_id
|
||||
task_id="$(echo "$response" | jq -r '.task_id // empty')"
|
||||
if [[ -z "$task_id" ]]; then
|
||||
echo "Error: No task_id in response" >&2; echo "$response" >&2; exit 1
|
||||
fi
|
||||
|
||||
echo "Task created: $task_id" >&2
|
||||
echo "$task_id"
|
||||
}
|
||||
|
||||
poll_task() {
|
||||
local task_id="$1"
|
||||
echo "Polling task $task_id..." >&2
|
||||
local start_time consecutive_failures=0
|
||||
start_time="$(date +%s)"
|
||||
|
||||
while true; do
|
||||
local now elapsed
|
||||
now="$(date +%s)"
|
||||
elapsed=$((now - start_time))
|
||||
if [[ $elapsed -gt $MAX_WAIT_TIME ]]; then
|
||||
echo "Error: Task $task_id timed out after ${MAX_WAIT_TIME}s" >&2; exit 1
|
||||
fi
|
||||
|
||||
local raw_output http_code response
|
||||
if raw_output="$(curl -s -w "\n%{http_code}" \
|
||||
-G "${API_BASE}/query/video_generation" \
|
||||
-d "task_id=$task_id" \
|
||||
-H "Authorization: Bearer ${MINIMAX_API_KEY}" \
|
||||
--max-time "$REQUEST_TIMEOUT" 2>/dev/null)"; then
|
||||
http_code="${raw_output##*$'\n'}"
|
||||
response="${raw_output%$'\n'*}"
|
||||
consecutive_failures=0
|
||||
else
|
||||
consecutive_failures=$((consecutive_failures + 1))
|
||||
echo " Poll error ($consecutive_failures/$MAX_CONSECUTIVE_FAILURES)" >&2
|
||||
if [[ $consecutive_failures -ge $MAX_CONSECUTIVE_FAILURES ]]; then
|
||||
echo "Error: Too many consecutive poll failures" >&2; exit 1
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"; continue
|
||||
fi
|
||||
|
||||
local status
|
||||
status="$(echo "$response" | jq -r '.status // "Unknown"')"
|
||||
echo " [${elapsed}s] Status: $status" >&2
|
||||
|
||||
if [[ "$status" == "Success" ]]; then
|
||||
local file_id
|
||||
file_id="$(echo "$response" | jq -r '.file_id // empty')"
|
||||
if [[ -z "$file_id" ]]; then
|
||||
echo "Error: Task succeeded but no file_id" >&2; exit 1
|
||||
fi
|
||||
echo "$file_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$status" == "Fail" || "$status" == "Failed" || "$status" == "Error" ]]; then
|
||||
local err_msg
|
||||
err_msg="$(echo "$response" | jq -r '.base_resp.status_msg // "Unknown error"')"
|
||||
echo "Error: Task failed: $err_msg" >&2; exit 1
|
||||
fi
|
||||
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
}
|
||||
|
||||
download_video() {
|
||||
local file_id="$1" output_path="$2"
|
||||
echo "Retrieving file $file_id..." >&2
|
||||
|
||||
local raw_output http_code response
|
||||
raw_output="$(curl -s -w "\n%{http_code}" \
|
||||
-G "${API_BASE}/files/retrieve" \
|
||||
-d "file_id=$file_id" \
|
||||
-H "Authorization: Bearer ${MINIMAX_API_KEY}" \
|
||||
--max-time "$REQUEST_TIMEOUT")"
|
||||
http_code="${raw_output##*$'\n'}"
|
||||
response="${raw_output%$'\n'*}"
|
||||
|
||||
local dl_url
|
||||
dl_url="$(echo "$response" | jq -r '.file.download_url // empty')"
|
||||
if [[ -z "$dl_url" ]]; then
|
||||
echo "Error: No download_url in file response" >&2; exit 1
|
||||
fi
|
||||
|
||||
echo "Downloading video..." >&2
|
||||
mkdir -p "$(dirname "$output_path")"
|
||||
curl -s -o "$output_path" --max-time $((REQUEST_TIMEOUT * 3)) "$dl_url"
|
||||
local size
|
||||
size="$(wc -c < "$output_path" | tr -d ' ')"
|
||||
echo "Video saved to: $output_path ($size bytes)" >&2
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
load_env
|
||||
check_api_key
|
||||
|
||||
local mode="" prompt="" model="" duration=10 resolution="768P"
|
||||
local first_frame="" last_frame="" subject_image=""
|
||||
local prompt_optimizer="" fast_pretreatment="" callback_url="" aigc_watermark=""
|
||||
local output=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode) mode="$2"; shift 2 ;;
|
||||
--prompt) prompt="$2"; shift 2 ;;
|
||||
--model) model="$2"; shift 2 ;;
|
||||
--duration) duration="$2"; shift 2 ;;
|
||||
--resolution) resolution="$2"; shift 2 ;;
|
||||
--first-frame) first_frame="$2"; shift 2 ;;
|
||||
--last-frame) last_frame="$2"; shift 2 ;;
|
||||
--subject-image) subject_image="$2"; shift 2 ;;
|
||||
--prompt-optimizer) prompt_optimizer="$2"; shift 2 ;;
|
||||
--fast-pretreatment) fast_pretreatment="$2"; shift 2 ;;
|
||||
--callback-url) callback_url="$2"; shift 2 ;;
|
||||
--aigc-watermark) aigc_watermark="$2"; shift 2 ;;
|
||||
-o|--output) output="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
MiniMax Video Generation CLI
|
||||
|
||||
Usage:
|
||||
generate_video.sh --mode MODE [options] -o OUTPUT
|
||||
|
||||
Modes:
|
||||
t2v Text-to-video
|
||||
i2v Image-to-video (requires --first-frame)
|
||||
sef Start-end frame (requires --first-frame and --last-frame)
|
||||
ref Subject reference (requires --subject-image)
|
||||
|
||||
Options:
|
||||
--mode MODE Generation mode: t2v, i2v, sef, ref (required)
|
||||
--prompt TEXT Text prompt describing the video
|
||||
--model MODEL Model name (default: T2V-01)
|
||||
--first-frame FILE First frame image (local file or URL)
|
||||
--last-frame FILE Last frame image (local file or URL)
|
||||
--subject-image FILE Subject reference image (local file or URL)
|
||||
-o, --output FILE Output video file (required)
|
||||
|
||||
Examples:
|
||||
generate_video.sh --mode t2v --prompt "A cat playing piano" -o cat.mp4
|
||||
generate_video.sh --mode i2v --prompt "Gentle breeze" --first-frame photo.jpg -o anim.mp4
|
||||
generate_video.sh --mode sef --first-frame start.jpg --last-frame end.jpg -o sef.mp4
|
||||
generate_video.sh --mode ref --prompt "Person dancing" --subject-image person.jpg -o ref.mp4
|
||||
USAGE
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$mode" ]]; then
|
||||
echo "Error: --mode is required (t2v, i2v, sef, ref)" >&2; exit 1
|
||||
fi
|
||||
if [[ -z "$output" ]]; then
|
||||
echo "Error: --output / -o is required" >&2; exit 1
|
||||
fi
|
||||
|
||||
# Default model per mode
|
||||
if [[ -z "$model" ]]; then
|
||||
case "$mode" in
|
||||
t2v) model="MiniMax-Hailuo-2.3" ;;
|
||||
i2v) model="MiniMax-Hailuo-2.3" ;;
|
||||
sef) model="MiniMax-Hailuo-02" ;;
|
||||
ref) model="S2V-01" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Build payload
|
||||
local payload
|
||||
payload=$(jq -n --arg m "$model" '{model: $m}')
|
||||
|
||||
[[ -n "$prompt" ]] && payload=$(echo "$payload" | jq --arg p "$prompt" '. + {prompt: $p}')
|
||||
payload=$(echo "$payload" | jq --argjson d "$duration" '. + {duration: $d}')
|
||||
payload=$(echo "$payload" | jq --arg r "$resolution" '. + {resolution: $r}')
|
||||
|
||||
[[ -n "$prompt_optimizer" ]] && payload=$(echo "$payload" | jq --argjson po "$(echo "$prompt_optimizer" | tr '[:upper:]' '[:lower:]' | jq -R 'test("true")')" '. + {prompt_optimizer: $po}')
|
||||
[[ -n "$callback_url" ]] && payload=$(echo "$payload" | jq --arg cu "$callback_url" '. + {callback_url: $cu}')
|
||||
[[ -n "$aigc_watermark" ]] && payload=$(echo "$payload" | jq --argjson aw "$aigc_watermark" '. + {aigc_watermark: $aw}')
|
||||
|
||||
case "$mode" in
|
||||
t2v) ;;
|
||||
i2v)
|
||||
if [[ -z "$first_frame" ]]; then
|
||||
echo "Error: --first-frame is required for i2v mode" >&2; exit 1
|
||||
fi
|
||||
local ff_url
|
||||
ff_url="$(resolve_image "$first_frame")"
|
||||
payload=$(echo "$payload" | jq --arg ff "$ff_url" '. + {first_frame_image: $ff}')
|
||||
[[ -n "$fast_pretreatment" ]] && payload=$(echo "$payload" | jq --argjson fp "$(echo "$fast_pretreatment" | tr '[:upper:]' '[:lower:]' | jq -R 'test("true")')" '. + {fast_pretreatment: $fp}')
|
||||
;;
|
||||
sef)
|
||||
if [[ -z "$first_frame" ]]; then
|
||||
echo "Error: --first-frame is required for sef mode" >&2; exit 1
|
||||
fi
|
||||
local ff_url
|
||||
ff_url="$(resolve_image "$first_frame")"
|
||||
payload=$(echo "$payload" | jq --arg ff "$ff_url" '. + {first_frame_image: $ff}')
|
||||
if [[ -n "$last_frame" ]]; then
|
||||
local lf_url
|
||||
lf_url="$(resolve_image "$last_frame")"
|
||||
payload=$(echo "$payload" | jq --arg lf "$lf_url" '. + {last_frame_image: $lf}')
|
||||
fi
|
||||
;;
|
||||
ref)
|
||||
if [[ -z "$subject_image" ]]; then
|
||||
echo "Error: --subject-image is required for ref mode" >&2; exit 1
|
||||
fi
|
||||
local si_url
|
||||
si_url="$(resolve_image "$subject_image")"
|
||||
payload=$(echo "$payload" | jq --arg si "$si_url" '. + {subject_reference: [{type: "character", image: [$si]}]}')
|
||||
if [[ -n "$first_frame" ]]; then
|
||||
local ff_url
|
||||
ff_url="$(resolve_image "$first_frame")"
|
||||
payload=$(echo "$payload" | jq --arg ff "$ff_url" '. + {first_frame_image: $ff}')
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown mode: $mode" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "Mode: $mode"
|
||||
echo "Model: $model"
|
||||
|
||||
local task_id file_id
|
||||
task_id="$(create_task "$payload")"
|
||||
file_id="$(poll_task "$task_id")"
|
||||
download_video "$file_id" "$output"
|
||||
echo "Done!"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user