mirror of
https://github.com/zhouxiaoka/autoclip.git
synced 2026-10-02 02:34:34 +08:00
Automatic output has no confirmation step and no client export, so the screening and production watches never settled and the only terminal event depended on the results page staying open. Screening now ends as auto_started once production begins, and a persisted studio-generation watch reports studio_generation_finished once with aggregate counts (variants, templates, speaker framing, packaging fallbacks, trims, burned captions) and backend-timed duration. The backend keeps the analysis run_id across phases, records generation.finished_at and marks automatic framing as framing_source=auto. Draft save/export carry template, style, tags switch and fallback; variant downloads, shares and ratings carry the platform, template, style and framing plus flow context. All new fields are allowlisted enums, counts or booleans; no titles, captions, names or tags leave the app. Also fix two issues found while rendering demos: deriving a draft for another platform now resets the title template version (Xiaohongshu after Douyin failed validation), and English caption segments and long model titles no longer force a packaging fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
62 lines
3.5 KiB
JavaScript
62 lines
3.5 KiB
JavaScript
const {test}=require('node:test')
|
|
const assert=require('node:assert/strict')
|
|
const fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'),ts=require('typescript')
|
|
|
|
function loadWorkflow(){
|
|
const source=fs.readFileSync(path.join(__dirname,'../src/analytics/workflow.ts'),'utf8')
|
|
const exports={}
|
|
vm.runInNewContext(ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText,{exports,Date,Math,JSON,Number,Array,Object,Map,Set,String,URL})
|
|
return exports
|
|
}
|
|
const workflow=loadWorkflow()
|
|
|
|
function tracker(){
|
|
const events=[],store=new Map()
|
|
const storage={getItem:k=>store.get(k)??null,setItem:(k,v)=>store.set(k,v),removeItem:k=>store.delete(k)}
|
|
const t=new workflow.WorkflowTracker(storage,()=>true,(name,props)=>{events.push({name,props});return true})
|
|
return {t,events}
|
|
}
|
|
|
|
const SNAPSHOT={
|
|
generation:{auto_start:true,status:'partial',requested_platforms:['douyin','tiktok'],skipped:[{strategy_id:'youtube_long',reason:'x'}],
|
|
source_has_burned_subtitles:false,created_at:'2026-09-30T10:00:00Z',finished_at:'2026-09-30T10:04:00Z'},
|
|
analysis:{status:'completed',phase:'rendering',run_id:'run-1'},
|
|
plan:{id:'plan-1',mode:'local',recommended_analysis:'subtitle'},
|
|
output_variants:[
|
|
{draft_id:'a',strategy_id:'douyin',status:'completed',framing:'speaker'},
|
|
{draft_id:'b',strategy_id:'tiktok',status:'failed',framing:'full_frame',trimmed_to_sec:180},
|
|
],
|
|
drafts:[{id:'a',title:'Private title',packaging:{template:'interview_zh',fallback:false}},{id:'b',packaging:{template:'podcast_en',fallback:true}}],
|
|
}
|
|
|
|
test('generation summary counts templates, framing and fallbacks without any content',()=>{
|
|
const summary=workflow.generationSummary(SNAPSHOT)
|
|
assert.deepEqual({...summary},{variant_count:2,completed_variant_count:1,failed_variant_count:1,skipped_variant_count:1,platform_count:2,
|
|
interview_count:1,podcast_count:1,landscape_count:0,speaker_framed_count:1,packaging_fallback_count:1,trimmed_count:1,burned_captions:false,duration_ms:240000})
|
|
const props=workflow.safeStudioProperties({...summary,title:'Private title',name:'Sam Altman',template:'interview_zh',packaging_style:'boxed',framing:'speaker'})
|
|
assert.equal(JSON.stringify(props).includes('Private'),false)
|
|
assert.equal(JSON.stringify(props).includes('Sam'),false)
|
|
assert.equal(props.template,'interview_zh'); assert.equal(props.packaging_style,'boxed'); assert.equal(props.framing,'speaker')
|
|
})
|
|
|
|
test('automatic output ends screening as auto_started, even if rendering later failed',()=>{
|
|
const {t,events}=tracker()
|
|
t.watch('studio-screen','p1',undefined,undefined,{},false,'run-1')
|
|
t.observeStudio(t.list().find(w=>w.kind==='studio-screen'),{...SNAPSHOT,analysis:{status:'failed',phase:'rendering',run_id:'run-1'}})
|
|
const screen=events.find(e=>e.name==='studio_screen_finished')
|
|
assert.ok(screen); assert.equal(screen.props.outcome,'auto_started')
|
|
})
|
|
|
|
test('generation watch reports one terminal event with the aggregate summary',()=>{
|
|
const {t,events}=tracker()
|
|
t.watch('studio-generation','p1')
|
|
const watch=t.list().find(w=>w.kind==='studio-generation')
|
|
t.observeStudio(watch,{...SNAPSHOT,generation:{...SNAPSHOT.generation,status:'rendering'}})
|
|
assert.equal(events.filter(e=>e.name==='studio_generation_finished').length,0)
|
|
t.observeStudio(watch,SNAPSHOT)
|
|
t.observeStudio(watch,SNAPSHOT)
|
|
const finished=events.filter(e=>e.name==='studio_generation_finished')
|
|
assert.equal(finished.length,1)
|
|
assert.equal(finished[0].props.outcome,'partial'); assert.equal(finished[0].props.speaker_framed_count,1)
|
|
})
|