CoolFace
Apppublic

SaifuddinHanif/ai-music-engineer

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
cli.js446 linesDownload Raw Back to src
1#!/usr/bin/env node2/**3 * AI Music Engineer — CLI4 * Usage:5 *   node src/cli.js ingest    <files...> [options]6 *   node src/cli.js analyze   <files...>7 *   node src/cli.js pipeline  <files...> [options]8 *   node src/cli.js status    <projectId>9 *   node src/cli.js report    <projectId>10 *   node src/cli.js projects11 */12 13"use strict";14 15const path  = require("path");16const fs    = require("fs");17const { Orchestrator }  = require("./index");18const AnalyzeModule     = require("./modules/analyze");19const Project           = require("./core/project");20const config            = require("../config/default");21 22// ─── ANSI colours ─────────────────────────────────────────────────────────────23const C = {24  reset:  "\x1b[0m",25  bold:   "\x1b[1m",26  dim:    "\x1b[2m",27  green:  "\x1b[32m",28  yellow: "\x1b[33m",29  red:    "\x1b[31m",30  cyan:   "\x1b[36m",31  blue:   "\x1b[34m",32  white:  "\x1b[37m",33  gray:   "\x1b[90m"34};35const c = (col, str) => `${C[col]}${str}${C.reset}`;36const hr = () => console.log(c("gray", "─".repeat(72)));37 38// ─── Argument parsing ─────────────────────────────────────────────────────────39function parseArgs(argv) {40  const args = { files: [], flags: {}, opts: {} };41  for (let i = 0; i < argv.length; i++) {42    const a = argv[i];43    if (a.startsWith("--")) {44      const [key, val] = a.slice(2).split("=");45      args.opts[key] = val !== undefined ? val : argv[i + 1] && !argv[i+1].startsWith("--") ? argv[++i] : true;46    } else if (a.startsWith("-")) {47      args.flags[a.slice(1)] = true;48    } else {49      args.files.push(a);50    }51  }52  return args;53}54 55// ─── Banner ───────────────────────────────────────────────────────────────────56function banner() {57  console.log(c("cyan", C.bold + `58  ╔═══════════════════════════════════════════╗59  ║      AI Music Engineer — MVP v1.0         ║60  ║  Ingest → Analyze → Mix → Master → Deliver║61  ╚═══════════════════════════════════════════╝` + C.reset));62  console.log();63}64 65// ─── Progress bar ─────────────────────────────────────────────────────────────66function progressBar(done, total, width = 40) {67  const pct  = total > 0 ? done / total : 0;68  const fill = Math.round(pct * width);69  const bar  = "█".repeat(fill) + "░".repeat(width - fill);70  return `[${bar}] ${done}/${total} (${Math.round(pct * 100)}%)`;71}72 73// ─── Main commands ────────────────────────────────────────────────────────────74 75async function cmdAnalyze(files) {76  if (!files.length) { console.error(c("red", "Usage: analyze <file1> [file2] ...")); process.exit(1); }77 78  banner();79  console.log(c("cyan", `Analyzing ${files.length} file(s)...`));80  hr();81 82  const analyzer = new AnalyzeModule();83  for (const fp of files) {84    const abs = path.resolve(fp);85    if (!fs.existsSync(abs)) { console.error(c("red", `File not found: ${fp}`)); continue; }86 87    console.log(c("bold", `\n▶ ${path.basename(fp)}`));88    try {89      const report = await analyzer.analyzeTrack(abs, { name: path.basename(fp) });90      printAnalysisReport(report);91    } catch (e) {92      console.error(c("red", `  Error: ${e.message}`));93    }94  }95  hr();96}97 98function printAnalysisReport(report) {99  const r = report.results || {};100 101  console.log(`  ${c("gray","Format:")}   ${r.format?.toUpperCase() || "?"} | ${r.sampleRate}Hz | ${r.bitDepth || "?"}bit | ${r.channels === 1 ? "Mono" : "Stereo"}`);102  if (r.duration) {103    const m = Math.floor(r.duration / 60), s = Math.round(r.duration % 60);104    console.log(`  ${c("gray","Duration:")} ${m}:${String(s).padStart(2,"0")}`);105  }106  if (r.lufs_integrated !== undefined) {107    const lufsOk = r.lufs_integrated <= -9;108    console.log(`  ${c("gray","Loudness:")} ${lufsOk ? c("green","✓") : c("yellow","⚠")} ${r.lufs_integrated?.toFixed(1)} LUFS integrated | True peak: ${r.true_peak_dbfs?.toFixed(1)} dBTP`);109  }110  if (r.peak_dbfs !== undefined) {111    const pkOk = r.peak_dbfs < -0.5;112    console.log(`  ${c("gray","Peak/RMS:")} ${pkOk ? c("green","✓") : c("red","✗")} ${r.peak_dbfs?.toFixed(2)} dBFS peak | ${r.mean_rms_dbfs?.toFixed(1)} dBFS RMS`);113  }114  if (r.phase_correlation !== undefined) {115    const phOk = r.phase_correlation > 0.3;116    console.log(`  ${c("gray","Stereo:")}   ${phOk ? c("green","✓") : c("yellow","⚠")} Phase corr: ${r.phase_correlation?.toFixed(2)} | Width est: ${r.stereo_width_estimate?.toFixed(2)}`);117  }118  if (r.spectrum) {119    const s = r.spectrum;120    console.log(`  ${c("gray","Spectrum:")} Low ${s.low_db?.toFixed(1)}dB | Mid ${s.mid_db?.toFixed(1)}dB | High ${s.high_db?.toFixed(1)}dB`);121  }122  if (report.issues?.length) {123    console.log(`  ${c("gray","Issues:")} (${report.issues.length})`);124    for (const issue of report.issues) {125      const icon = issue.severity === "error" ? c("red","✗") : issue.severity === "warning" ? c("yellow","⚠") : c("gray","·");126      console.log(`    ${icon} ${issue.message}`);127    }128  } else {129    console.log(`  ${c("green","✓ No issues detected")}`);130  }131  if (report.suggestedFixes?.length) {132    console.log(`  ${c("cyan","Suggested fixes:")}`);133    for (const fix of report.suggestedFixes) {134      console.log(`    ${c("cyan","→")} ${fix.fix}`);135    }136  }137}138 139async function cmdIngest(files, opts) {140  if (!files.length) { console.error(c("red", "Usage: ingest <files...> [--artist NAME] [--title TITLE]")); process.exit(1); }141 142  banner();143  const orch = new Orchestrator();144 145  const project = orch.createProject({146    artist: opts.artist || "Unknown Artist",147    title:  opts.title  || path.basename(files[0], path.extname(files[0])),148    genre:  opts.genre  || null,149    bpm:    opts.bpm    ? parseInt(opts.bpm) : null,150    notes:  opts.notes  || null,151    destinations: opts.dest ? opts.dest.split(",") : ["streaming"],152    options: {153      stemSeparation: opts.separate === "true" || opts.separate === "1",154      stemPreset: opts.stems || "4stem",155      autonomyLevel: parseInt(opts.autonomy || "2")156    }157  });158 159  orch.addFiles(project, files);160 161  console.log(c("cyan", `Project: ${project.id}`));162  console.log(c("cyan", `"${project.title}" by ${project.artist}`));163  hr();164  console.log(c("bold", "Running Ingest stage..."));165 166  const result = await orch.runStage(project, "INGEST");167  const ingestResult = result.stages?.INGEST?.result || {};168 169  console.log(c("green", `\n✓ Ingested ${ingestResult.filesProcessed || 0} file(s)`));170  if (ingestResult.issues?.length) {171    for (const issue of ingestResult.issues) {172      console.log(c("yellow", `  ⚠ ${issue.message || issue}`));173    }174  }175  console.log(`\n  Project saved to: ${c("cyan", project.projectDir)}`);176  console.log(`  Project ID: ${c("bold", project.id)}`);177  hr();178  return project;179}180 181async function cmdPipeline(files, opts) {182  if (!files.length) { console.error(c("red", "Usage: pipeline <files...> [--artist NAME] [--title TITLE] ...")); process.exit(1); }183 184  banner();185  const orch = new Orchestrator();186 187  const project = orch.createProject({188    artist: opts.artist || "Unknown Artist",189    title:  opts.title  || path.basename(files[0], path.extname(files[0])),190    genre:  opts.genre  || null,191    album:  opts.album  || null,192    bpm:    opts.bpm    ? parseInt(opts.bpm) : null,193    key:    opts.key    || null,194    notes:  opts.notes  || null,195    references: opts.refs ? opts.refs.split(",").map(r => r.trim()).filter(Boolean) : [],196    trackSequence: opts.sequence197      ? opts.sequence.split(",").map((t, i) => ({ title: t.trim(), gapMs: 2000, trackNumber: i + 1 }))198      : [],199    destinations: opts.dest ? opts.dest.split(",") : ["streaming"],200    sampleRate: parseInt(opts.sr || "44100"),201    bitDepth:   parseInt(opts.bd || "24"),202    options: {203      stemSeparation: opts.separate === "true" || opts.separate === "1",204      stemPreset:     opts.stems    || "4stem",205      autonomyLevel:  parseInt(opts.autonomy || "2"),206      fadeInMs:       parseInt(opts.fadein   || "50"),207      fadeOutMs:      parseInt(opts.fadeout  || "2000"),208      changeBudget:   { eq_max_db: parseFloat(opts.eqbudget || "6") },209      lockedTracks:   opts.locked ? opts.locked.split(",").map(s => s.trim()) : []210    }211  });212 213  orch.addFiles(project, files);214  if (opts.refs) console.log(c("cyan", `Reference tracks: ${opts.refs}`));215 216  console.log(c("cyan", `Project: ${project.id}`));217  console.log(c("cyan", `"${project.title}" by ${project.artist}`));218  console.log(c("gray", `Autonomy: L${project.options.autonomyLevel} | Destinations: ${project.destinations.join(", ")}`));219  hr();220 221  // Track stage timing222  const stageTimings = {};223 224  const result = await orch.run(project, {225    fromStage: opts.from || "INGEST",226    autonomyLevel: parseInt(opts.autonomy || "2"),227    onEvent: (event, data) => {228      switch (event) {229        case "stage_start":230          stageTimings[data.stage] = Date.now();231          process.stdout.write(`\n  ${c("blue","▶")} ${c("bold", data.stage.padEnd(14))} `);232          break;233        case "stage_complete":234          const dur = stageTimings[data.stage] ? `${((Date.now() - stageTimings[data.stage]) / 1000).toFixed(1)}s` : "";235          console.log(c("green", `✓`) + c("gray", ` ${dur}`));236          if (data.result?.summary) console.log(`     ${c("gray", data.result.summary)}`);237          break;238        case "stage_failed":239          console.log(c("red", `✗ FAILED: ${data.error}`));240          break;241        case "stage_retry":242          console.log(c("yellow", `↺ Retry ${data.attempt}`));243          break;244        case "escalation":245          console.log(c("red", `\n  🚨 ${data.message}`));246          if (data.userChoices) data.userChoices.forEach(ch => console.log(c("gray", `     → ${ch}`)));247          if (data.fallback) console.log(c("cyan", `  ↪ Fallback: ${data.fallback}`));248          break;249        case "missing_info":250          console.log(c("yellow", `\n  ⚠ Missing info — proceeding with safe defaults:`));251          (data.questions||[]).forEach(q => console.log(c("gray", `     ? ${q.question}\n       Default: ${q.default}`)));252          break;253        case "approval_required":254          console.log(c("yellow", `\n  ⏸ Approval required for ${data.stage}`));255          if (data.summary?.plainLanguageSummary) console.log(c("gray", `     ${data.summary.plainLanguageSummary}`));256          console.log(c("gray", "  (L2 mode — auto-proceeding)"));257          break;258      }259    }260  });261 262  console.log();263  hr();264  printPipelineSummary(result.summary, project);265  hr();266  console.log(`\n  Project dir: ${c("cyan", project.projectDir)}`);267  console.log(`  Project ID:  ${c("bold",  project.id)}`);268  console.log();269}270 271function printPipelineSummary(summary, project) {272  if (!summary) return;273  const { progress, stages } = summary;274 275  console.log(c("bold", "\n  Pipeline Summary"));276  console.log(`  ${progressBar(progress.done, progress.total)}\n`);277 278  const stageNames = Object.keys(stages);279  for (const stage of stageNames) {280    const s = stages[stage];281    const icon = s.status === "complete" ? c("green","✓")282               : s.status === "skipped"  ? c("gray","–")283               : s.status === "failed"   ? c("red","✗")284               : c("yellow","?");285    const dur = s.duration ? c("gray", ` (${s.duration})`) : "";286    const retry = s.retries > 0 ? c("yellow", ` [${s.retries} retries]`) : "";287    console.log(`  ${icon} ${stage.padEnd(14)} ${s.status.padEnd(10)}${dur}${retry}`);288  }289 290  // Show output files291  if (project.outputs?.length) {292    console.log(`\n  ${c("bold","Output files:")} (${project.outputs.length})`);293    for (const out of project.outputs) {294      console.log(`  ${c("gray","·")} [${out.stage}] ${out.filename}`);295      if (out.lufs) console.log(`    ${c("gray", `${out.lufs?.toFixed(1)} LUFS | ${out.truePeak?.toFixed(1)} dBTP`)}`);296    }297  }298}299 300async function cmdStatus(projectId) {301  const orch = new Orchestrator();302  const projectsRoot = config.paths.projects;303  const projectDir = path.join(projectsRoot, projectId);304 305  if (!fs.existsSync(path.join(projectDir, "project.json"))) {306    console.error(c("red", `Project not found: ${projectId}`));307    process.exit(1);308  }309 310  const project = orch.loadProject(projectDir);311  banner();312  console.log(c("bold", `Project: ${project.title} by ${project.artist}`));313  console.log(c("gray", `ID: ${project.id} | Created: ${project.createdAt}`));314  hr();315  console.log(c("bold", "Tracks:"));316  for (const t of project.tracks) {317    console.log(`  ${c("gray","·")} [${t.role}] ${t.name} (${t.format?.toUpperCase() || "?"})`);318  }319  hr();320  console.log(c("bold", "QC Gates:"));321  for (const [gate, result] of Object.entries(project.qcResults || {})) {322    const icon = result.passed ? c("green","✓") : c("red","✗");323    console.log(`  ${icon} ${gate.toUpperCase()}: ${result.passed ? "PASSED" : "FAILED"}`);324    if (result.issues?.length) {325      for (const i of result.issues.slice(0,3)) {326        console.log(`    ${c("gray","·")} ${i.message || i}`);327      }328    }329  }330  hr();331  console.log(c("bold", "Outputs:"));332  for (const out of project.outputs || []) {333    console.log(`  ${c("cyan","·")} ${out.filename}`);334  }335}336 337async function cmdReport(projectId) {338  const orch = new Orchestrator();339  const projectsRoot = config.paths.projects;340  const projectDir = path.join(projectsRoot, projectId);341  const project = orch.loadProject(projectDir);342 343  banner();344  if (project.analysisReport) {345    console.log(AnalyzeModule.formatReport(project.analysisReport));346  } else {347    console.log(c("yellow", "No analysis report found. Run the analyze stage first."));348  }349}350 351async function cmdProjects() {352  const orch = new Orchestrator();353  const projects = orch.listProjects();354  banner();355  if (!projects.length) {356    console.log(c("gray", "No projects found."));357    return;358  }359  console.log(c("bold", `Projects (${projects.length}):`));360  hr();361  for (const p of projects) {362    console.log(`  ${c("cyan", p.id)}`);363    console.log(`    "${p.title}" by ${p.artist}`);364    console.log(`    ${c("gray", `Created: ${p.createdAt} | Tracks: ${p.tracks.length} | Outputs: ${p.outputs.length}`)}`);365  }366}367 368function printHelp() {369  banner();370  console.log(c("bold", "Commands:"));371  console.log(`372  ${c("cyan","analyze")}  <file(s)>                  Analyze audio files — no project created373  ${c("cyan","ingest")}   <file(s)> [opts]            Ingest files into a new project374  ${c("cyan","pipeline")} <file(s)> [opts]            Run full pipeline on files375  ${c("cyan","status")}   <projectId>                Show project status376  ${c("cyan","report")}   <projectId>                Show analysis report377  ${c("cyan","projects")}                             List all projects378 379${c("bold","Options for ingest/pipeline:")}380  --artist "Name"          Artist name381  --title  "Title"         Track/project title382  --album  "Album"         Album / EP name383  --genre  "Genre"         Genre (optional)384  --key    "C major"       Musical key (optional)385  --bpm    120             BPM (optional; auto-detected if blank)386  --notes  "vocal forward" Creative direction notes387  --refs   "/ref.wav"      Reference track path(s) for mix matching (comma-separated)388  --dest   streaming,cd    Delivery destinations (comma-separated)389  --sr     44100           Sample rate (default: 44100)390  --bd     24              Bit depth (default: 24)391  --autonomy 2             Autonomy level L0–L3 (default: 2)392  --separate true          Enable stem separation393  --stems  4stem           Stem preset: 2stem, 4stem, 6stem, 8stem394  --sequence "T1,T2,T3"   Track running order for EP/album delivery (comma-separated titles)395  --from   ANALYZE         Start pipeline from a specific stage396  --fadein  50             Fade-in duration ms (default: 50)397  --fadeout 2000           Fade-out duration ms (default: 2000)398  --eqbudget 6             Max EQ change ±dB per band (default: 6)399  --locked "Drums,Guitar"  Locked tracks — will not be processed400 401${c("bold","Destinations:")}402  streaming  -14 LUFS / -1 dBTP    (Spotify, Apple Music)403  video      -14 LUFS / -1 dBTP    (Film, YouTube)404  cd         -9 LUFS  / -0.1 dBTP405  club       -6 LUFS  / -0.3 dBTP406  broadcast  -23 LUFS / -1 dBTP    (EBU R128)407  podcast    -16 LUFS / -1 dBTP408 409${c("bold","Examples:")}410  node src/cli.js analyze song.wav411  node src/cli.js pipeline song.wav --artist "Jordan" --title "Skyline" --dest streaming,cd412  node src/cli.js pipeline song.mp3 --notes "vocal forward, warm" --refs "/ref.wav" --autonomy 1413  node src/cli.js pipeline mix.wav --dest streaming --fadein 100 --fadeout 3000 --eqbudget 3414  node src/cli.js pipeline stems/*.wav --artist "Mia" --separate true --stems 4stem415  node src/cli.js status proj_1234567_abcd416  `);417}418 419// ─── Entry point ──────────────────────────────────────────────────────────────420 421async function main() {422  const [,, cmd, ...rest] = process.argv;423  const { files, opts } = parseArgs(rest);424 425  // Ensure projects dir exists426  fs.mkdirSync(config.paths.projects, { recursive: true });427 428  try {429    switch (cmd) {430      case "analyze":   return await cmdAnalyze(files);431      case "ingest":    return await cmdIngest(files, opts);432      case "pipeline":  return await cmdPipeline(files, opts);433      case "status":    return await cmdStatus(files[0] || opts.id);434      case "report":    return await cmdReport(files[0] || opts.id);435      case "projects":  return await cmdProjects();436      default:          return printHelp();437    }438  } catch (err) {439    console.error(c("red", `\n✗ Error: ${err.message}`));440    if (process.env.DEBUG) console.error(err.stack);441    process.exit(1);442  }443}444 445main();446