takumi-cli 1.2.7 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,7 +45,7 @@ takumi setup
45
45
 
46
46
  | Command | Description |
47
47
  |---------|-------------|
48
- | `takumi convert <path> [crf] [max_height]` | Convert to FireTV-optimized H.264 MP4 |
48
+ | `takumi convert <path> [--profile name]` | Convert to optimized MP4 |
49
49
  | `takumi trim <video> <start> <end>` | Cut a clip between timestamps |
50
50
  | `takumi cc <path> [lang] [model] [format]` | Generate captions using Whisper |
51
51
  | `takumi thumb <path> [timestamp]` | Extract a poster image (JPG) |
@@ -60,11 +60,20 @@ All commands accept a single file or a folder (processes all videos recursively)
60
60
  ### Examples
61
61
 
62
62
  ```bash
63
- # Convert all videos in a folder for FireTV
64
- takumi convert ./videos
63
+ # Convert for web (default, plays everywhere)
64
+ takumi convert video.mp4
65
65
 
66
- # Higher quality conversion, max 720p
67
- takumi convert ./videos 21 720
66
+ # Convert for FireTV
67
+ takumi convert ./videos --profile firetv
68
+
69
+ # Compressed for email or Slack
70
+ takumi convert video.mp4 --profile small
71
+
72
+ # High quality for portfolio
73
+ takumi convert video.mp4 --profile hq
74
+
75
+ # Custom: override quality and max height
76
+ takumi convert ./videos --crf 21 --max 720
68
77
 
69
78
  # Trim a 75-second clip
70
79
  takumi trim video.mp4 00:01:30 00:02:45
package/bin/takumi-mcp CHANGED
@@ -54,5 +54,8 @@ if [ -z "$NODE" ]; then
54
54
  exit 1
55
55
  fi
56
56
 
57
- SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
57
+ if [ -z "${SCRIPT_DIR:-}" ]; then
58
+ REAL_PATH="$(realpath "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo "$0")"
59
+ SCRIPT_DIR="$(cd "$(dirname "$REAL_PATH")/.." && pwd)"
60
+ fi
58
61
  exec "$NODE" "$SCRIPT_DIR/mcp/server.js"
@@ -1,5 +1,12 @@
1
1
  #!/bin/bash
2
- # ── Convert: FireTV-optimized MP4 ────────────────────────────────────────────
2
+ # ── Convert: Video conversion with profile presets ──────────────────────────
3
+
4
+ # ── Profiles ────────────────────────────────────────────────────────────────
5
+ #
6
+ # web (default) — Web-optimized MP4. Plays everywhere.
7
+ # firetv — FireTV-optimized H.264 MP4. High Profile, mod16 dimensions.
8
+ # small — Compressed for email, Slack, low bandwidth. 720p, higher CRF.
9
+ # hq — High quality for portfolio or client delivery. Low CRF.
3
10
 
4
11
  SIZES_16_9=("1920:1080" "1280:720" "1024:576" "768:432" "512:288" "256:144")
5
12
  SIZES_4_3=("640:480" "576:432" "512:384" "448:336" "384:288" "320:240" "256:192" "192:144" "128:96")
@@ -26,12 +33,66 @@ get_best_mod16() {
26
33
  echo "${SIZES[-1]}"
27
34
  }
28
35
 
36
+ apply_profile() {
37
+ local PROFILE="$1"
38
+ case "$PROFILE" in
39
+ web)
40
+ PROFILE_CRF=23
41
+ PROFILE_MAX_H=1080
42
+ PROFILE_LEVEL=""
43
+ PROFILE_PROFILE="main"
44
+ PROFILE_SUFFIX="_web"
45
+ PROFILE_MOD16=false
46
+ PROFILE_AUDIO_BITRATE="128k"
47
+ ;;
48
+ firetv)
49
+ PROFILE_CRF=23
50
+ PROFILE_MAX_H=1080
51
+ PROFILE_LEVEL="4.0"
52
+ PROFILE_PROFILE="high"
53
+ PROFILE_SUFFIX="_firetv"
54
+ PROFILE_MOD16=true
55
+ PROFILE_AUDIO_BITRATE="128k"
56
+ ;;
57
+ small)
58
+ PROFILE_CRF=28
59
+ PROFILE_MAX_H=720
60
+ PROFILE_LEVEL=""
61
+ PROFILE_PROFILE="main"
62
+ PROFILE_SUFFIX="_small"
63
+ PROFILE_MOD16=false
64
+ PROFILE_AUDIO_BITRATE="96k"
65
+ ;;
66
+ hq)
67
+ PROFILE_CRF=18
68
+ PROFILE_MAX_H=2160
69
+ PROFILE_LEVEL=""
70
+ PROFILE_PROFILE="high"
71
+ PROFILE_SUFFIX="_hq"
72
+ PROFILE_MOD16=false
73
+ PROFILE_AUDIO_BITRATE="192k"
74
+ ;;
75
+ *)
76
+ echo "Unknown profile: $PROFILE"
77
+ echo "Available: web, firetv, small, hq"
78
+ return 1
79
+ ;;
80
+ esac
81
+ }
82
+
29
83
  convert_file() {
30
- local VIDEO="$1" CRF="$2" MAX_H="$3"
84
+ local VIDEO="$1" CRF="$2" MAX_H="$3" PROFILE="$4"
85
+
86
+ apply_profile "$PROFILE" || return 1
87
+
88
+ # Allow explicit overrides
89
+ CRF="${CRF:-$PROFILE_CRF}"
90
+ MAX_H="${MAX_H:-$PROFILE_MAX_H}"
91
+
31
92
  local DIR BASENAME OUTPUT
32
93
  DIR="$(dirname "$VIDEO")"
33
94
  BASENAME="$(basename "${VIDEO%.*}")"
34
- OUTPUT="${DIR}/${BASENAME}${CONVERT_SUFFIX}.mp4"
95
+ OUTPUT="${DIR}/${BASENAME}${PROFILE_SUFFIX}.mp4"
35
96
 
36
97
  if [ -f "$OUTPUT" ]; then
37
98
  echo "⏭️ Skipping (output exists): $(basename "$VIDEO")"
@@ -40,7 +101,7 @@ convert_file() {
40
101
 
41
102
  echo ""
42
103
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
43
- echo "🎬 Processing: $(basename "$VIDEO")"
104
+ echo "🎬 Processing: $(basename "$VIDEO") [${PROFILE}]"
44
105
 
45
106
  local SRC_W SRC_H
46
107
  SRC_W=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of csv=p=0 "$VIDEO" 2>/dev/null)
@@ -51,18 +112,29 @@ convert_file() {
51
112
  return 1
52
113
  fi
53
114
 
54
- local TARGET TGT_W TGT_H
55
- TARGET=$(get_best_mod16 "$SRC_W" "$SRC_H" "$MAX_H")
56
- TGT_W="${TARGET%%:*}"
57
- TGT_H="${TARGET##*:}"
115
+ local SCALE_FILTER
116
+ if [ "$PROFILE_MOD16" = true ]; then
117
+ local TARGET TGT_W TGT_H
118
+ TARGET=$(get_best_mod16 "$SRC_W" "$SRC_H" "$MAX_H")
119
+ TGT_W="${TARGET%%:*}"
120
+ TGT_H="${TARGET##*:}"
121
+ SCALE_FILTER="scale=${TGT_W}:${TGT_H}"
122
+ echo " ${SRC_W}x${SRC_H} → ${TGT_W}x${TGT_H} (mod16) | CRF: $CRF"
123
+ else
124
+ SCALE_FILTER="scale=-2:'min(${MAX_H},ih)'"
125
+ echo " ${SRC_W}x${SRC_H} → max ${MAX_H}p | CRF: $CRF"
126
+ fi
58
127
 
59
- echo " ${SRC_W}x${SRC_H} → ${TGT_W}x${TGT_H} (mod16) | CRF: $CRF"
128
+ local LEVEL_FLAGS=""
129
+ if [ -n "$PROFILE_LEVEL" ]; then
130
+ LEVEL_FLAGS="-level $PROFILE_LEVEL"
131
+ fi
60
132
 
61
133
  if ffmpeg -i "$VIDEO" \
62
134
  -c:v libx264 -preset slow -crf "$CRF" \
63
- -profile:v high -level 4.0 -pix_fmt yuv420p \
64
- -vf "scale=${TGT_W}:${TGT_H}" -r 30 \
65
- -c:a aac -b:a 128k -ac 2 -ar 44100 \
135
+ -profile:v "$PROFILE_PROFILE" $LEVEL_FLAGS -pix_fmt yuv420p \
136
+ -vf "$SCALE_FILTER" \
137
+ -c:a aac -b:a "$PROFILE_AUDIO_BITRATE" -ac 2 -ar 44100 \
66
138
  -movflags +faststart \
67
139
  -y -loglevel warning -stats \
68
140
  "$OUTPUT" 2>&1; then
@@ -78,25 +150,63 @@ convert_file() {
78
150
  }
79
151
 
80
152
  cmd_convert() {
81
- local INPUT="${1:-}"
82
- local CRF="${2:-23}"
83
- local MAX_H="${3:-1080}"
153
+ local INPUT=""
154
+ local PROFILE="web"
155
+ local CRF=""
156
+ local MAX_H=""
157
+
158
+ # Parse arguments
159
+ while [ $# -gt 0 ]; do
160
+ case "$1" in
161
+ --profile) PROFILE="$2"; shift 2 ;;
162
+ --crf) CRF="$2"; shift 2 ;;
163
+ --max) MAX_H="$2"; shift 2 ;;
164
+ --help|-h|help)
165
+ echo "Usage: takumi convert <file_or_folder> [options]"
166
+ echo ""
167
+ echo "Options:"
168
+ echo " --profile <name> Conversion profile (default: web)"
169
+ echo " --crf <value> Quality override (18-28, lower = better)"
170
+ echo " --max <height> Max height in pixels"
171
+ echo ""
172
+ echo "Profiles:"
173
+ echo " web Web-optimized MP4. Plays everywhere. (default)"
174
+ echo " firetv FireTV-optimized. High Profile, mod16 dimensions."
175
+ echo " small Compressed for email/Slack. 720p, smaller file."
176
+ echo " hq High quality for portfolio or client delivery."
177
+ echo ""
178
+ echo "Examples:"
179
+ echo " takumi convert video.mp4"
180
+ echo " takumi convert ./videos --profile firetv"
181
+ echo " takumi convert ./videos --profile small"
182
+ echo " takumi convert video.mp4 --crf 21 --max 720"
183
+ return 0
184
+ ;;
185
+ *)
186
+ if [ -z "$INPUT" ]; then
187
+ INPUT="$1"
188
+ fi
189
+ shift
190
+ ;;
191
+ esac
192
+ done
84
193
 
85
194
  if [ -z "$INPUT" ]; then
86
- echo "Usage: ./takumi.sh convert <file_or_folder> [crf] [max_height]"
87
- echo " crf: 18-28 (default: 23, lower = better quality)"
88
- echo " max_height: max output height (default: 1080)"
195
+ echo "Usage: takumi convert <file_or_folder> [--profile web|firetv|small|hq] [--crf N] [--max N]"
89
196
  return 1
90
197
  fi
91
198
 
199
+ # Validate profile
200
+ apply_profile "$PROFILE" || return 1
201
+
92
202
  if [ -f "$INPUT" ]; then
93
- convert_file "$INPUT" "$CRF" "$MAX_H"
203
+ convert_file "$INPUT" "$CRF" "$MAX_H" "$PROFILE"
94
204
  elif [ -d "$INPUT" ]; then
95
205
  echo "📂 Scanning: $INPUT"
96
- echo "⚙️ CRF: $CRF | Max: ${MAX_H}p | H.264 MP4"
206
+ echo "⚙️ Profile: $PROFILE | CRF: ${CRF:-$PROFILE_CRF} | Max: ${MAX_H:-$PROFILE_MAX_H}p"
97
207
  while IFS= read -r file; do
98
- convert_file "$file" "$CRF" "$MAX_H"
99
- done < <(find_videos "$INPUT" "$CONVERT_SUFFIX")
208
+ convert_file "$file" "$CRF" "$MAX_H" "$PROFILE"
209
+ done < <(find_videos "$INPUT" "$PROFILE_SUFFIX")
100
210
  echo ""
101
211
  echo "🏁 Done!"
102
212
  else
package/mcp/server.js CHANGED
@@ -11,12 +11,18 @@ const BASH = findBash();
11
11
  const TAKUMI = path.resolve(__dirname, "..", "takumi.sh");
12
12
  const ENV = { ...process.env, SCRIPT_DIR: path.resolve(__dirname, "..") };
13
13
 
14
+ function result(text, isError = false) {
15
+ return {
16
+ content: [{ type: "text", text }],
17
+ isError,
18
+ };
19
+ }
20
+
14
21
  function run(args) {
15
22
  if (!BASH) {
16
- return Promise.resolve({
17
- text: "Error: bash not found. On Windows, install Git for Windows: https://git-scm.com/download/win",
18
- isError: true,
19
- });
23
+ return Promise.resolve(
24
+ result("Error: bash not found. On Windows, install Git for Windows: https://git-scm.com/download/win", true)
25
+ );
20
26
  }
21
27
 
22
28
  // Validate that file path args exist before running
@@ -24,10 +30,7 @@ function run(args) {
24
30
  if (filePath) {
25
31
  const fs = require("fs");
26
32
  if (!fs.existsSync(filePath)) {
27
- return Promise.resolve({
28
- text: `Error: file or folder not found: ${filePath}`,
29
- isError: true,
30
- });
33
+ return Promise.resolve(result(`Error: file or folder not found: ${filePath}`, true));
31
34
  }
32
35
  }
33
36
 
@@ -35,11 +38,11 @@ function run(args) {
35
38
  execFile(BASH, [TAKUMI, ...args], { timeout: 300_000, env: ENV }, (err, stdout, stderr) => {
36
39
  const output = [stdout, stderr].filter(Boolean).join("\n").trim();
37
40
  if (err && !output) {
38
- resolve({ text: `Error (exit code ${err.code}): ${err.message}`, isError: true });
41
+ resolve(result(`Error (exit code ${err.code}): ${err.message}`, true));
39
42
  } else if (err) {
40
- resolve({ text: `Error (exit code ${err.code}):\n${output}`, isError: true });
43
+ resolve(result(`Error (exit code ${err.code}):\n${output}`, true));
41
44
  } else {
42
- resolve({ text: output || "Done.", isError: false });
45
+ resolve(result(output || "Done."));
43
46
  }
44
47
  });
45
48
  });
@@ -71,16 +74,18 @@ async function main() {
71
74
 
72
75
  server.tool(
73
76
  "takumi_convert",
74
- "Convert videos to FireTV-optimized H.264 MP4",
77
+ "Convert videos to optimized MP4. Choose a profile based on the user's intent:\n- 'web' (default): website, CMS, landing page, blog, general use, online publishing, embedding in a webpage\n- 'firetv': FireTV app, Amazon Fire tablet, streaming device, TV app assets, broadcast, set-top box\n- 'small': email attachment, Slack, Teams, Discord, SMS, quick preview, social sharing, file size matters, low bandwidth, mobile messaging\n- 'hq': portfolio, client deliverable, showreel, demo reel, presentation, archival, best possible quality",
75
78
  {
76
79
  path: z.string().describe("Path to video file or folder"),
77
- crf: z.number().optional().describe("Quality (lower = better, default 23)"),
78
- max_height: z.number().optional().describe("Max height in pixels (default 1080)"),
80
+ profile: z.enum(["web", "firetv", "small", "hq"]).optional().describe("Conversion profile. 'web': website/CMS/general use/embedding. 'firetv': FireTV/streaming device/TV app. 'small': email/Slack/Teams/Discord/messaging/quick share/preview/low bandwidth. 'hq': portfolio/client delivery/showreel/presentation/archival. Default: web"),
81
+ crf: z.number().optional().describe("Quality override (18-28, lower = better)"),
82
+ max_height: z.number().optional().describe("Max height override in pixels"),
79
83
  },
80
- async ({ path: p, crf, max_height }) => {
84
+ async ({ path: p, profile, crf, max_height }) => {
81
85
  const args = ["convert", p];
82
- if (crf !== undefined) args.push(String(crf));
83
- if (max_height !== undefined) args.push(String(max_height));
86
+ if (profile) args.push("--profile", profile);
87
+ if (crf !== undefined) args.push("--crf", String(crf));
88
+ if (max_height !== undefined) args.push("--max", String(max_height));
84
89
  return run(args);
85
90
  }
86
91
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "takumi-cli",
3
- "version": "1.2.7",
3
+ "version": "1.3.1",
4
4
  "description": "The craftsman's toolkit for shaping video assets",
5
5
  "bin": {
6
6
  "takumi": "bin/takumi.js",
package/takumi.sh CHANGED
@@ -4,7 +4,7 @@
4
4
  # ============================================================================
5
5
  # Commands:
6
6
  # cc Generate closed captions (SRT/VTT) using Whisper
7
- # convert Convert videos to FireTV-optimized H.264 MP4 (mod16)
7
+ # convert Convert videos to optimized MP4 (web, firetv, small, hq)
8
8
  # trim Cut a clip between timestamps
9
9
  # thumb Extract poster image from video
10
10
  # info Show video metadata
@@ -27,7 +27,7 @@ set -euo pipefail
27
27
  SCRIPT_DIR="$(dirname "$(realpath "$0")")"
28
28
 
29
29
  VIDEO_EXTENSIONS="mp4|mov|avi|mkv|webm|m4v|flv|wmv|mpg|mpeg|ts"
30
- CONVERT_SUFFIX="_firetv"
30
+ CONVERT_SUFFIX="_web"
31
31
 
32
32
  # ── Helpers ──────────────────────────────────────────────────────────────────
33
33
 
@@ -56,7 +56,7 @@ show_help() {
56
56
  echo " update Update to latest version"
57
57
  echo " mcp-config Print MCP server config"
58
58
  echo " cc <path> [lang] [model] [format] Generate captions from video"
59
- echo " convert <path> [crf] [max_height] Convert to FireTV H.264 MP4"
59
+ echo " convert <path> [--profile <name>] Convert to optimized MP4"
60
60
  echo " trim <video> <start> <end> Cut a clip between timestamps"
61
61
  echo " thumb <path> [timestamp] Extract poster image (JPG)"
62
62
  echo " info <path> Show video metadata"
@@ -68,7 +68,7 @@ show_help() {
68
68
  echo "Examples:"
69
69
  echo " ./takumi.sh setup"
70
70
  echo " ./takumi.sh cc ./videos ja"
71
- echo " ./takumi.sh convert ./videos 21"
71
+ echo " ./takumi.sh convert ./videos --profile firetv"
72
72
  echo " ./takumi.sh trim video.mp4 00:01:30 00:02:45"
73
73
  echo " ./takumi.sh thumb video.mp4 00:00:15"
74
74
  echo " ./takumi.sh info ./videos"
package/test.sh CHANGED
@@ -131,6 +131,38 @@ assert "640x480 capped 240 → 320:240" "320:240" "$OUT"
131
131
  OUT=$(get_best_mod16 3840 2160 1080)
132
132
  assert "4K capped 1080 → 1920:1080" "1920:1080" "$OUT"
133
133
 
134
+ # ── Convert profiles ───────────────────────────────────────────────────────
135
+
136
+ echo ""
137
+ echo "🔧 Convert profiles"
138
+
139
+ source commands/convert.sh
140
+
141
+ apply_profile web
142
+ assert "web profile CRF" "23" "$PROFILE_CRF"
143
+ assert "web profile suffix" "_web" "$PROFILE_SUFFIX"
144
+ assert "web profile mod16 off" "false" "$PROFILE_MOD16"
145
+
146
+ apply_profile firetv
147
+ assert "firetv profile CRF" "23" "$PROFILE_CRF"
148
+ assert "firetv profile suffix" "_firetv" "$PROFILE_SUFFIX"
149
+ assert "firetv profile mod16 on" "true" "$PROFILE_MOD16"
150
+
151
+ apply_profile small
152
+ assert "small profile CRF" "28" "$PROFILE_CRF"
153
+ assert "small profile max height" "720" "$PROFILE_MAX_H"
154
+ assert "small profile suffix" "_small" "$PROFILE_SUFFIX"
155
+
156
+ apply_profile hq
157
+ assert "hq profile CRF" "18" "$PROFILE_CRF"
158
+ assert "hq profile suffix" "_hq" "$PROFILE_SUFFIX"
159
+
160
+ OUT=$(apply_profile badprofile 2>&1)
161
+ assert "bad profile shows error" "Unknown profile" "$OUT"
162
+
163
+ OUT=$(run convert video.mp4 --help)
164
+ assert "convert --help shows profiles" "Profiles:" "$OUT"
165
+
134
166
  # ── Paths with spaces ──────────────────────────────────────────────────────
135
167
 
136
168
  echo ""