@longzai-intelligence-music-video/cli 0.0.2 → 0.0.4
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/dist/index.js +8 -8
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import"./rolldown-runtime-BqT_7tdF.js";import{createCli as e,defineCommand as t}from"@longzai-intelligence-cli/core";import{clackInteraction as n,picocolorsColor as r}from"@longzai-intelligence-cli/standard";import*as i from"zod";import{CONFIG_FILENAME as
|
|
2
|
-
`)}`})}let
|
|
3
|
-
`),r=(n[0]??``).split(`,`).map(e=>Number(e)),i=r[0],a=r[1],o=Number(n[1]??NaN);return i===void 0||a===void 0||!Number.isFinite(i)||!Number.isFinite(a)||!Number.isFinite(o)?
|
|
4
|
-
`);return}for(let e of t.value.entries)process.stdout.write(e.ok?`${e.id}:${e.title}(${e.template})\n`:`[失败] ${e.error}\n`)}}),t(Y,{name:`project verify`,description:`一致性体检:结构/媒体文件/模板参数/MIDI 通道覆盖/时长对齐(缺省全量项目)`,arguments:[{signature:`[projectId]`,description:`项目 id(缺省全量体检)`}],options:[$,Q],schema:i.object({projectId:i.string().optional(),config:i.string().optional(),format:X}),action:async(e,t)=>{let n=await
|
|
5
|
-
`),process.exitCode=1;return}let r=await
|
|
1
|
+
import"./rolldown-runtime-BqT_7tdF.js";import{createCli as e,defineCommand as t}from"@longzai-intelligence-cli/core";import{clackInteraction as n,picocolorsColor as r}from"@longzai-intelligence-cli/standard";import*as i from"zod";import{createConfigController as a}from"@longzai-intelligence-markdown/data-engine";import{CONFIG_FILENAME as o,buildRenderProps as s,failure as c,listProjects as l,loadProject as u,parseMidiTimeline as d,probeAudioDurationSec as f,resolveRuntimeEnv as p,runVerify as m,scaffoldConfigText as h,stageProjectMedia as g,success as _}from"@longzai-intelligence-music-video/core";import{discoverTemplates as v,listTemplates as y,loadTemplate as b,validateTemplateParams as x,writeRenderEntry as S}from"@longzai-intelligence-music-video/templates";import{tmpdir as C}from"node:os";import{dirname as w,join as T,resolve as ee}from"node:path";import{bundle as E}from"@remotion/bundler";import{renderMedia as D,selectComposition as O}from"@remotion/renderer";import{existsSync as k,mkdirSync as A,readFileSync as te,unlinkSync as ne,writeFileSync as re}from"node:fs";import{fileURLToPath as ie}from"node:url";import{spawnSync as j}from"node:child_process";const M=a({configFilename:o,scaffold:e=>h({dataRoot:e.dataRoot}),resolveRuntimeEnv:p});function N(e,t={}){return M.configInit(e,t)}async function P(e){let t=await M.configShow(e);return t.ok?{ok:!0,resolution:t.value}:{ok:!1,error:t.error}}function F(e){return M.configPath(e)}function I(){return{stagingDir:T(C(),`lzi-mv-debug-noop`),audioUrl:null,assetUrls:{}}}async function L(e){let t=await p(e.configPath);if(!t.ok)return c({message:t.error.message});let n=u(t.value.env.dataRoot,e.projectId);if(!n.ok)return c({message:n.error.message});let r=n.value,i=await b(t.value.env.dataRoot,r.frontmatter.template);if(!i.ok)return c({message:i.message});let a=i.template.debug?.dumpFrame;if(a===void 0)return c({message:`模板 ${r.frontmatter.template} 未提供 debug.dumpFrame 钩子(模板调试协议为可选能力)`});let o=x(i.template,r.frontmatter.templateParams);if(!o.ok)return c({message:o.message});if(r.midiFile===void 0)return c({message:`缺少 MIDI 乐谱(${e.projectId} score/*.mid)`});let l=await d(r.midiFile.absPath,r.frontmatter.tracks);if(!l.ok)return c({message:l.error.message});let m=null;if(r.audioFile!==void 0){let e=await f(r.audioFile.absPath);if(!e.ok)return c({message:e.error.message});m=e.value}let h=s({project:r,validatedTemplateParams:o.params,rawTimeline:l.value,audioDurationSec:m,staged:I()}),g=a({projectId:e.projectId,dataRoot:t.value.env.dataRoot,projectDir:r.dir,atSec:e.atSec,width:h.width,height:h.height,params:o.params,timeline:h.timeline});return _({projectId:e.projectId,templateId:r.frontmatter.template,lines:g.lines,...g.data===void 0?{}:{data:g.data}})}async function R(e){let t=await p(e);if(!t.ok)return c({message:t.error.message});let n=l(t.value.env.dataRoot);return _({dataRoot:t.value.env.dataRoot,entries:n.map(e=>e.ok?{id:e.value.id,title:e.value.frontmatter.title,template:e.value.frontmatter.template,ok:!0}:{id:`(解析失败)`,title:`-`,template:`-`,ok:!1,error:e.error.message})})}async function z(e,t){let n=await p(t);if(!n.ok)return c({message:n.error.message});let r=await m(n.value.env.dataRoot,e);return r.ok?_({dataRoot:n.value.env.dataRoot,reports:r.value.reports.map(e=>({projectId:e.projectId,issues:e.issues}))}):c({message:r.error.message})}function B(){let e=ie(new URL(`.`,import.meta.url)),t=ee(e,`..`,`..`,`remotion`);return{appDir:t,entryPoint:T(t,`.generated`,`entry.tsx`),publicDir:T(t,`public`),stagingRoot:T(t,`public`,`.staging`)}}const V={gl:`angle`};function H(){let e=process.env.LZI_MUSIC_VIDEO_BROWSER_EXECUTABLE;return typeof e==`string`&&e.length>0?e:void 0}async function U(e,t){let n=await p(t);if(!n.ok)return c({message:n.error.message});let r=u(n.value.env.dataRoot,e);if(!r.ok)return c({message:r.error.message});let i=r.value,a=await m(n.value.env.dataRoot,e);if(!a.ok)return c({message:a.error.message});if(!a.value.ok){let e=a.value.reports.flatMap(e=>e.issues.filter(e=>e.severity===`error`)).map(e=>e.message);return c({message:`体检未通过(拒绝渲染):\n${e.map(e=>`- ${e}`).join(`
|
|
2
|
+
`)}`})}let o=a.value.reports.flatMap(e=>e.issues.filter(e=>e.severity===`warning`)),l=await b(n.value.env.dataRoot,i.frontmatter.template);if(!l.ok)return c({message:l.message});let h=x(l.template,i.frontmatter.templateParams);if(!h.ok)return c({message:h.message});if(i.midiFile===void 0)return c({message:`缺少 MIDI 乐谱(score/*.mid)`});let v=await d(i.midiFile.absPath,i.frontmatter.tracks);if(!v.ok)return c({message:v.error.message});let y=null;if(i.audioFile!==void 0){let e=await f(i.audioFile.absPath);if(!e.ok)return c({message:e.error.message});y=e.value}let S=B(),C=g({dataRoot:n.value.env.dataRoot,project:i,assetParams:l.template.assetParams,stagingRoot:S.stagingRoot});if(!C.ok)return c({message:C.error.message});let w=s({project:i,validatedTemplateParams:h.params,rawTimeline:v.value,audioDurationSec:y,staged:C.value});return _({project:i,props:w,staged:C.value,warnings:o})}async function W(e){let t=await U(e.projectId,e.configPath);if(!t.ok)return c(t.error);let n=B(),r=await p(e.configPath);if(!r.ok)return c({message:r.error.message});S({templates:v(r.value.env.dataRoot),outputPath:n.entryPoint});let i=await E({entryPoint:n.entryPoint}),a=H(),o=await O({serveUrl:i,id:t.value.project.frontmatter.template,inputProps:t.value.props,chromiumOptions:V,...a===void 0?{}:{browserExecutable:a}}),s=e.out??T(t.value.project.dir,`renders`,`${t.value.project.frontmatter.template}.mp4`);A(w(s),{recursive:!0});let l=t=>{e.onProgress?.(t.progress)};return await D({composition:o,serveUrl:i,codec:e.codec??`h264`,inputProps:t.value.props,outputLocation:s,scale:e.scale,chromiumOptions:V,...a===void 0?{}:{browserExecutable:a},onProgress:l}),_({outputLocation:s,durationInFrames:t.value.props.durationInFrames,warnings:t.value.warnings})}async function G(e,t){let n=await U(e,t);if(!n.ok)return c(n.error);let r=await p(t);if(!r.ok)return c({message:r.error.message});let i=B();S({templates:v(r.value.env.dataRoot),outputPath:i.entryPoint});let a=T(n.value.staged.stagingDir,`props.json`);re(a,`${JSON.stringify(n.value.props,null,2)}\n`);let o=await Bun.spawn({cmd:[`bunx`,`remotion`,`studio`,i.entryPoint,`--props`,a],cwd:i.appDir,stdout:`inherit`,stderr:`inherit`,stdin:`inherit`}).exited;return _({exitedCode:o,propsPath:a})}async function K(e){let t=await p(e);return t.ok?{ok:!0,value:{dataRoot:t.value.env.dataRoot,entries:await y(t.value.env.dataRoot)}}:{ok:!1,error:{message:t.error.message}}}function q(e){let t=j(`ffprobe`,[`-v`,`error`,`-select_streams`,`v:0`,`-show_entries`,`stream=width,height:format=duration`,`-of`,`csv=p=0`,e],{encoding:`utf8`});if(t.status!==0)return c({message:`ffprobe 失败(${e}):${t.stderr}`});let n=(t.stdout??``).trim().split(`
|
|
3
|
+
`),r=(n[0]??``).split(`,`).map(e=>Number(e)),i=r[0],a=r[1],o=Number(n[1]??NaN);return i===void 0||a===void 0||!Number.isFinite(i)||!Number.isFinite(a)||!Number.isFinite(o)?c({message:`ffprobe 解析失败(${e}):${t.stdout}`}):_({width:i,height:a,durationSec:o})}function J(e,t){let n=T(C(),`lzi-mv-verify-frame-${process.pid}-${Date.now()}.raw`),r=j(`ffmpeg`,[`-y`,`-v`,`error`,`-ss`,String(t),`-i`,e,`-frames:v`,`1`,`-f`,`rawvideo`,`-pix_fmt`,`rgb24`,n],{encoding:`utf8`});if(r.status!==0)return c({message:`ffmpeg 抽帧失败(t=${t}s):${r.stderr}`});let i=te(n);ne(n);let a=new Uint8Array(i);return _({pixels:a})}async function ae(e){let t=await p(e.configPath);if(!t.ok)return c({message:t.error.message});let n=u(t.value.env.dataRoot,e.projectId);if(!n.ok)return c({message:n.error.message});let r=n.value,i=await b(t.value.env.dataRoot,r.frontmatter.template);if(!i.ok)return c({message:i.message});let a=i.template.debug?.verifyRender;if(a===void 0)return c({message:`模板 ${r.frontmatter.template} 未提供 debug.verifyRender 钩子(模板调试协议为可选能力)`});let o=e.video??T(r.dir,`renders`,`${r.frontmatter.template}.mp4`);if(!k(o))return c({message:`成片不存在:${o}(先 render 出片,或 --video 指定路径)`});let s=q(o);if(!s.ok)return c({message:s.error.message});let l=s.value,d=Math.max(1,e.samples??3),f=e=>{let t=J(o,e);if(!t.ok)throw Error(t.error.message);return a.analyzeFrame({width:l.width,height:l.height,atSec:e,durationSec:l.durationSec,pixels:t.value.pixels})},m;try{m=f(l.durationSec*.08)}catch(e){return c({message:e instanceof Error?e.message:String(e)})}let h=[];for(let e=1;e<=d;e+=1){let t=Math.max(l.durationSec*.25,l.durationSec*e/(d+1));try{h.push(f(t))}catch(e){return c({message:e instanceof Error?e.message:String(e)})}}let g=a.judge({reference:m,observations:h});return _({video:o,width:l.width,height:l.height,durationSec:l.durationSec,sampleCount:d,pass:g.pass,summaryLines:g.summaryLines})}const Y=e({name:`lzi-music-video`,description:`模板化音乐视频合成工具(数据仓驱动:project 管理/render 出片/studio 预览;双仓架构裁定见仓 ADR 0001)`,interaction:n,color:r}),X=i.enum([`text`,`json`]).default(`text`);function Z(e){process.stdout.write(`${JSON.stringify(e,null,2)}\n`)}const Q={flags:`--format <kind>`,description:`输出格式 text|json`,defaultValue:`text`},$={flags:`--config <path>`,description:`配置文件路径(缺省自 cwd 向上查找 lzi-music-video.config.ts;配置文件必备,缺失即报错)`};t(Y,{name:`project list`,description:`列出数据仓内全部音乐项目(projects/<song-id>/ 扫描)`,options:[$,Q],schema:i.object({config:i.string().optional(),format:X}),action:async e=>{let t=await R(e.config);if(e.format===`json`){Z(t);return}if(!t.ok){process.stderr.write(`${t.error.message}\n`),process.exitCode=1;return}if(process.stdout.write(`数据根:${t.value.dataRoot}\n`),t.value.entries.length===0){process.stdout.write(`(空:无音乐项目)
|
|
4
|
+
`);return}for(let e of t.value.entries)process.stdout.write(e.ok?`${e.id}:${e.title}(${e.template})\n`:`[失败] ${e.error}\n`)}}),t(Y,{name:`project verify`,description:`一致性体检:结构/媒体文件/模板参数/MIDI 通道覆盖/时长对齐(缺省全量项目)`,arguments:[{signature:`[projectId]`,description:`项目 id(缺省全量体检)`}],options:[$,Q],schema:i.object({projectId:i.string().optional(),config:i.string().optional(),format:X}),action:async(e,t)=>{let n=await z(t[0],e.config);if(e.format===`json`){Z(n);return}if(!n.ok){process.stderr.write(`${n.error.message}\n`),process.exitCode=1;return}process.stdout.write(`数据根:${n.value.dataRoot}\n`);for(let e of n.value.reports){let t=e.issues.filter(e=>e.severity===`error`).length;process.stdout.write(`${e.projectId}:${t===0?`通过`:`${t} 项错误`}\n`);for(let t of e.issues)process.stdout.write(` [${t.severity}] ${t.message}\n`)}}}),t(Y,{name:`render`,description:`渲染出片(默认输出 <项目目录>/renders/<template>.mp4——产物内聚项目文件夹;首次渲染自动下载 Chrome Headless Shell)`,arguments:[{signature:`<projectId>`,description:`项目 id`}],options:[{flags:`--out <path>`,description:`输出路径覆盖(缺省落位项目内 renders/)`},{flags:`--scale <n>`,description:`分辨率缩放(缺省 1;0.5 = 半分辨率快速预览)`},{flags:`--codec <kind>`,description:`编码 h264|vp8|vp9(缺省 h264)`},$,Q],schema:i.object({out:i.string().optional(),scale:i.coerce.number().min(.1).max(4).optional(),codec:i.enum([`h264`,`vp8`,`vp9`]).optional(),config:i.string().optional(),format:X}),action:async(e,t)=>{let n=t[0];if(n===void 0){process.stderr.write(`缺少项目 id:lzi-music-video render <projectId>
|
|
5
|
+
`),process.exitCode=1;return}let r=await W({projectId:n,configPath:e.config,out:e.out,scale:e.scale,codec:e.codec,onProgress:e=>{process.stdout.write(`\r渲染进度:${Math.round(e)}%`)}});if(process.stdout.write(`
|
|
6
6
|
`),e.format===`json`){Z(r);return}if(!r.ok){process.stderr.write(`${r.error.message}\n`),process.exitCode=1;return}process.stdout.write(`已渲染:${r.value.outputLocation}(${r.value.durationInFrames} 帧)\n`);for(let e of r.value.warnings)process.stdout.write(` [warning] ${e.message}\n`)}}),t(Y,{name:`studio`,description:`打开 Remotion Studio 预览项目(staging + props 与渲染同源,所见即所得)`,arguments:[{signature:`<projectId>`,description:`项目 id`}],options:[$],schema:i.object({config:i.string().optional()}),action:async(e,t)=>{let n=t[0];if(n===void 0){process.stderr.write(`缺少项目 id:lzi-music-video studio <projectId>
|
|
7
|
-
`),process.exitCode=1;return}let r=await
|
|
7
|
+
`),process.exitCode=1;return}let r=await G(n,e.config);if(!r.ok){process.stderr.write(`${r.error.message}\n`),process.exitCode=1;return}if(r.value.exitedCode!==0){process.stderr.write(`remotion studio 退出码:${r.value.exitedCode}\n`),process.exitCode=1;return}}}),t(Y,{name:`template list`,description:`列出数据根内发现的全部模板(templates/<id>/ 装载;id = Composition id,project.md template 取值域)`,options:[$,Q],schema:i.object({config:i.string().optional(),format:X}),action:async e=>{let t=await K(e.config);if(e.format===`json`){Z(t);return}if(!t.ok){process.stderr.write(`${t.error.message}\n`),process.exitCode=1;return}if(process.stdout.write(`数据根:${t.value.dataRoot}\n`),t.value.entries.length===0){process.stdout.write(`(空:数据根无模板)
|
|
8
8
|
`);return}for(let e of t.value.entries)e.ok?process.stdout.write(`${e.entry.id}:${e.entry.label}\n ${e.entry.description}\n`):process.stdout.write(`[装载失败] ${e.id}:${e.message}\n`)}}),t(Y,{name:`debug frame`,description:`转储任意时刻的场景模型帧(项目解析链与渲染同源:时间轴/画幅/参数均为引擎真源;需模板声明 debug.dumpFrame 钩子)`,arguments:[{signature:`<projectId>`,description:`项目 id`}],options:[{flags:`--at <sec>`,description:`采样时刻(秒,音频时间轴)`},$,Q],schema:i.object({at:i.coerce.number().min(0),config:i.string().optional(),format:X}),action:async(e,t)=>{let n=t[0];if(n===void 0){process.stderr.write(`缺少项目 id:lzi-music-video debug frame <projectId> --at <sec>
|
|
9
|
-
`),process.exitCode=1;return}let r=await
|
|
10
|
-
`),process.exitCode=1;return}let r=await
|
|
9
|
+
`),process.exitCode=1;return}let r=await L({projectId:n,atSec:e.at,configPath:e.config});if(e.format===`json`){Z(r),r.ok||(process.exitCode=1);return}if(!r.ok){process.stderr.write(`${r.error.message}\n`),process.exitCode=1;return}process.stdout.write(`项目 ${r.value.projectId}(模板 ${r.value.templateId}):\n`);for(let e of r.value.lines)process.stdout.write(`${e}\n`)}}),t(Y,{name:`verify render`,description:`渲样成片像素基线验收(引擎抽帧喂给、模板判据——拦渲染运行时静默失效;需模板声明 debug.verifyRender 钩子;缺省受验 <项目目录>/renders/<template>.mp4)`,arguments:[{signature:`<projectId>`,description:`项目 id`}],options:[{flags:`--video <path>`,description:`受验成片路径覆盖(缺省项目 renders/ 内聚产物)`},{flags:`--samples <n>`,description:`采样帧数(缺省 3,沿时长均匀分位)`},$,Q],schema:i.object({video:i.string().optional(),samples:i.coerce.number().int().min(1).max(32).optional(),config:i.string().optional(),format:X}),action:async(e,t)=>{let n=t[0];if(n===void 0){process.stderr.write(`缺少项目 id:lzi-music-video verify render <projectId>
|
|
10
|
+
`),process.exitCode=1;return}let r=await ae({projectId:n,video:e.video,samples:e.samples,configPath:e.config});if(e.format===`json`){Z(r),process.exitCode=r.ok&&!r.value.pass?1:+!r.ok;return}if(!r.ok){process.stderr.write(`${r.error.message}\n`),process.exitCode=1;return}process.stdout.write(`渲样像素基线验收:${r.value.video}(${r.value.width}×${r.value.height},${r.value.durationSec.toFixed(1)}s,采样 ${r.value.sampleCount} 帧)\n`);for(let e of r.value.summaryLines)process.stdout.write(`${e}\n`);process.exitCode=+!r.value.pass}}),t(Y,{name:`config init`,description:`生成 lzi-music-video.config.ts(数据根就地配置入口;已存在则提示,不覆盖)`,options:[{flags:`--data-root <path>`,description:`数据根(缺省 '.',即就地配置:在数据根目录执行时自指)`}],schema:i.object({dataRoot:i.string().optional()}),action:async e=>{let t=N(process.cwd(),{dataRoot:e.dataRoot});if(!t.ok){process.stderr.write(`${t.error.message}\n`),process.exitCode=1;return}process.stdout.write(`已生成:${t.path}(渲染浏览器如需指向本机 Chrome,在 .env 配置 LZI_MUSIC_VIDEO_BROWSER_EXECUTABLE;缺省由 Remotion 自动管理 Headless Shell)\n`)}}),t(Y,{name:`config show`,description:`展示生效配置(配置文件必备承载 dataRoot;zod 校验 + 绝对路径)`,options:[$,Q],schema:i.object({config:i.string().optional(),format:X}),action:async e=>{let t=await P(e.config);if(e.format===`json`){Z(t);return}if(!t.ok){process.stderr.write(`${t.error.message}\n`),process.exitCode=1;return}process.stdout.write(`通道:${t.resolution.channel}${t.resolution.configPath===void 0?``:`(${t.resolution.configPath})`}\ndataRoot=${t.resolution.env.dataRoot}\n`)}}),t(Y,{name:`config path`,description:`打印配置文件与 .env 双通道位置`,schema:i.object({}),action:async()=>{let e=F(process.cwd());process.stdout.write(`${e.configPath}${e.configExists?``:`(不存在,可先 config init)`}\n${e.envPath}${e.envExists?``:`(不存在)`}\n`)}}),await Y.parseAsync(process.argv);export{};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longzai-intelligence-music-video/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Longzai Intelligence Music Video CLI - 音乐视频合成命令行工具(project/render/studio/template/config 命令族)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@longzai-intelligence-cli/core": "^0.0.10",
|
|
28
28
|
"@longzai-intelligence-cli/standard": "^0.0.10",
|
|
29
|
-
"@longzai-intelligence-
|
|
30
|
-
"@longzai-intelligence-music-video/
|
|
29
|
+
"@longzai-intelligence-markdown/data-engine": "^0.0.1",
|
|
30
|
+
"@longzai-intelligence-music-video/core": "0.0.4",
|
|
31
|
+
"@longzai-intelligence-music-video/templates": "0.0.4",
|
|
31
32
|
"@remotion/bundler": "^4.0.0",
|
|
32
33
|
"@remotion/renderer": "^4.0.0",
|
|
33
34
|
"zod": "^4.4.3"
|