app.html 210 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558
  1. <div class="app-container theme-day">
  2. <!-- 头部导航 -->
  3. <header class="header shadow">
  4. <span class="logo">抖音AI视频生成系统 <small>v1.0</small></span>
  5. <span class="spacer"></span>
  6. <button class="account-chip" type="button" (click)="setCurrentTab(auth.isLoggedIn ? 'user-center' : 'login')">
  7. <span class="account-chip__dot">{{ auth.isLoggedIn ? (auth.currentUser?.displayName || auth.currentUser?.username || 'U').charAt(0) : '未' }}</span>
  8. <span class="account-chip__text">{{ auth.isLoggedIn ? (auth.currentUser?.displayName || auth.currentUser?.username) : '登录' }}</span>
  9. <span class="account-chip__mode" *ngIf="auth.localMode">测试</span>
  10. </button>
  11. </header>
  12. <!-- 主内容区域 -->
  13. <div class="main-content">
  14. <!-- 侧边栏导航(P1 重构:3 大分区纯字排版,参考 components/app-sidebar) -->
  15. <app-sidebar
  16. *ngIf="auth.isLoggedIn"
  17. [currentTab]="currentTab"
  18. (tabChange)="setCurrentTab($any($event))">
  19. </app-sidebar>
  20. <!-- 工作区内容 -->
  21. <main class="workbench">
  22. <ng-container *ngIf="!auth.isLoggedIn">
  23. <app-auth-page
  24. mode="login"
  25. [allowRegister]="false"
  26. (modeChange)="setCurrentTab($event)"
  27. (signedIn)="onAuthSignedIn()">
  28. </app-auth-page>
  29. </ng-container>
  30. <ng-container *ngIf="auth.isLoggedIn">
  31. <!-- 首屏 Hero(CTA-first,P2) -->
  32. <app-home *ngIf="currentTab === 'home'"
  33. [monitorSummary]="getHomeMonitorSummary()"
  34. (navigate)="onHomeNavigate($event)"
  35. (suggestionPick)="onAssistantSuggestion($event)"
  36. (inlineChatChange)="homeChatActive = $event">
  37. </app-home>
  38. <!-- 生成模式中心(Pipelines Hub) -->
  39. <section class="dh-page pipeline-hub-page" *ngIf="currentTab === 'pipelines'">
  40. <header class="dh-page-header">
  41. <div class="dh-header-icon">
  42. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  43. <rect x="3" y="3" width="7" height="7" rx="1"></rect>
  44. <rect x="14" y="3" width="7" height="7" rx="1"></rect>
  45. <rect x="3" y="14" width="7" height="7" rx="1"></rect>
  46. <rect x="14" y="14" width="7" height="7" rx="1"></rect>
  47. </svg>
  48. </div>
  49. <div class="dh-header-text">
  50. <h2>生成模式中心</h2>
  51. <p>选择一个 AI 视频生成模式开始创作。已上线模式可直接进入;规划中模式即将推出。</p>
  52. </div>
  53. </header>
  54. <div class="pipeline-gallery">
  55. <article *ngFor="let p of pipelines"
  56. class="pipeline-card"
  57. [class.is-stable]="p.status === 'stable'"
  58. [class.is-beta]="p.status === 'beta'"
  59. [class.is-planned]="p.status === 'planned'"
  60. (click)="p.status !== 'planned' && setCurrentTab(p.route)">
  61. <div class="pipeline-card__top">
  62. <div class="pipeline-card__icon">
  63. <svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" [innerHTML]="p.iconSvg"></svg>
  64. </div>
  65. <span class="pipeline-card__badge"
  66. [class.badge-stable]="p.status === 'stable'"
  67. [class.badge-beta]="p.status === 'beta'"
  68. [class.badge-planned]="p.status === 'planned'">
  69. {{ p.status === 'planned' ? '即将推出' : '可使用' }}
  70. </span>
  71. </div>
  72. <h3 class="pipeline-card__title">{{ p.displayName }}</h3>
  73. <p class="pipeline-card__desc">{{ p.description }}</p>
  74. <div class="pipeline-card__hint">
  75. <span class="pipeline-card__hint-label">输入</span>
  76. <span class="pipeline-card__hint-value">{{ p.inputHint }}</span>
  77. </div>
  78. <button class="pipeline-card__cta" type="button"
  79. [disabled]="p.status === 'planned'"
  80. (click)="$event.stopPropagation(); p.status !== 'planned' && setCurrentTab(p.route)">
  81. {{ p.status === 'planned' ? '敬请期待' : '进入模式 →' }}
  82. </button>
  83. </article>
  84. </div>
  85. </section>
  86. <!-- 图生视频 Pipeline (P1) -->
  87. <app-image-generation *ngIf="currentTab === 'image-generation'"></app-image-generation>
  88. <app-text-to-video *ngIf="currentTab === 'video-generation'"></app-text-to-video>
  89. <app-batch-production *ngIf="currentTab === 'batch-production'"></app-batch-production>
  90. <app-retrospective *ngIf="currentTab === 'retrospective'"></app-retrospective>
  91. <!-- 图生视频 Pipeline (P1) -->
  92. <app-image-to-video *ngIf="currentTab === 'image-to-video'"></app-image-to-video>
  93. <!-- 动作迁移 Pipeline (P2) -->
  94. <app-action-transfer *ngIf="currentTab === 'action-transfer'"></app-action-transfer>
  95. <!-- 素材合成视频 Pipeline (P3) -->
  96. <app-asset-remix *ngIf="currentTab === 'asset-remix'"></app-asset-remix>
  97. <!-- 主题生视频 Pipeline (P4) -->
  98. <app-topic-to-video *ngIf="currentTab === 'topic-to-video'"></app-topic-to-video>
  99. <!-- 我的创作(跨 Pipeline 草稿与 artifact 时间线) -->
  100. <app-drafts *ngIf="currentTab === 'drafts' && auth.isLoggedIn"
  101. (navigateToPipeline)="onDraftsNavigate($event)">
  102. </app-drafts>
  103. <!-- 素材库(跨 Pipeline AI 生成成果聚合) -->
  104. <app-library *ngIf="currentTab === 'library'"
  105. (navigateToDraft)="onLibraryNavigate($event)"
  106. (navigateToRetrospective)="onLibraryRetrospective($event)">
  107. </app-library>
  108. <app-templates *ngIf="currentTab === 'templates'"></app-templates>
  109. <app-auth-page *ngIf="currentTab === 'login'"
  110. mode="login"
  111. [allowRegister]="false"
  112. (modeChange)="setCurrentTab($event)"
  113. (signedIn)="onAuthSignedIn()">
  114. </app-auth-page>
  115. <app-auth-page *ngIf="currentTab === 'register'"
  116. mode="register"
  117. [allowRegister]="false"
  118. (modeChange)="setCurrentTab($event)"
  119. (signedIn)="onAuthSignedIn()">
  120. </app-auth-page>
  121. <app-user-center *ngIf="currentTab === 'user-center'"
  122. (navigate)="setCurrentTab($any($event))">
  123. </app-user-center>
  124. <!-- 视频发现模块 -->
  125. <section class="dh-page search-page" *ngIf="currentTab === 'search'">
  126. <header class="dh-page-header">
  127. <div class="dh-header-icon">
  128. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  129. <circle cx="11" cy="11" r="8"></circle>
  130. <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
  131. </svg>
  132. </div>
  133. <div class="dh-header-text">
  134. <h1>视频发现</h1>
  135. <p>搜索抖音视频,获取 AI 创作灵感与素材</p>
  136. </div>
  137. </header>
  138. <div class="search-controls">
  139. <div class="search-input-group">
  140. <div class="form-field search-field">
  141. <label>输入关键词搜索抖音视频</label>
  142. <input type="text" placeholder="例如:科技博主、AI新闻、产品评测..."
  143. [(ngModel)]="searchKeyword">
  144. </div>
  145. <div class="form-field filter-field">
  146. <label>排序方式</label>
  147. <select [(ngModel)]="sortType" (ngModelChange)="onSearchFilterChange()">
  148. <option value="0">综合排序</option>
  149. <option value="1">最多点赞</option>
  150. <option value="2">最新发布</option>
  151. </select>
  152. </div>
  153. <div class="form-field filter-field">
  154. <label>发布时间</label>
  155. <select [(ngModel)]="publishTime" (ngModelChange)="onSearchFilterChange()">
  156. <option value="0">不限</option>
  157. <option value="1">一天内</option>
  158. <option value="7">一周内</option>
  159. <option value="180">半年内</option>
  160. </select>
  161. </div>
  162. <div class="form-field filter-field">
  163. <label>视频时长</label>
  164. <select [(ngModel)]="filterDuration" (ngModelChange)="onSearchFilterChange()">
  165. <option value="0">不限</option>
  166. <option value="0-1">1分钟内</option>
  167. <option value="1-5">1-5分钟</option>
  168. <option value="5-10000">5分钟以上</option>
  169. </select>
  170. </div>
  171. <button class="btn btn-primary search-btn"
  172. (click)="searchVideos()" [disabled]="!searchKeyword">
  173. <span class="icon">search</span> 搜索视频
  174. </button>
  175. </div>
  176. </div>
  177. <div class="video-link-analysis-card">
  178. <div class="video-link-analysis-copy">
  179. <span class="video-link-analysis-kicker">快速分析</span>
  180. <strong>粘贴抖音视频链接,直接进入爆款分析</strong>
  181. </div>
  182. <div class="video-link-analysis-form">
  183. <input type="text"
  184. placeholder="粘贴抖音视频链接、分享文案或作品 ID"
  185. [(ngModel)]="videoLinkAnalysisInput"
  186. (keyup.enter)="analyzeVideoLink()">
  187. <button class="btn btn-primary" type="button"
  188. (click)="analyzeVideoLink()"
  189. [disabled]="!videoLinkAnalysisInput.trim() || isAnalyzingVideoLink || isAnalyzingViralVideo">
  190. <span class="icon">{{isAnalyzingVideoLink ? 'hourglass_empty' : 'link'}}</span>
  191. {{isAnalyzingVideoLink ? '解析中...' : '解析并分析'}}
  192. </button>
  193. </div>
  194. <div class="video-link-analysis-error" *ngIf="videoLinkAnalysisError">{{videoLinkAnalysisError}}</div>
  195. </div>
  196. <div class="video-results" *ngIf="searchResults.length > 0">
  197. <div class="results-header">
  198. <h3>搜索结果 ({{filteredSearchResults.length}} / {{searchResults.length}})</h3>
  199. <button class="btn" (click)="loadMore()" *ngIf="hasMore">
  200. <span class="icon">refresh</span> 加载更多
  201. </button>
  202. </div>
  203. <div class="video-grid" *ngIf="filteredSearchResults.length > 0">
  204. <div class="video-card" *ngFor="let video of filteredSearchResults">
  205. <div class="video-thumbnail">
  206. <img [src]="video.cover_url" [alt]="video.desc" referrerpolicy="no-referrer" (error)="handleVideoCoverError(video)">
  207. <div class="video-duration">{{video.duration | timeFormat}}</div>
  208. </div>
  209. <div class="card-content">
  210. <h4 class="video-title">{{video.desc}}</h4>
  211. <div class="video-info-row">
  212. <div class="video-meta-row">
  213. <span class="video-meta-chip" *ngIf="video.publishDate">
  214. {{video.publishDate | date:'MM-dd HH:mm'}}
  215. </span>
  216. <span class="video-meta-chip" *ngIf="video.duration">
  217. {{video.duration | timeFormat}}
  218. </span>
  219. <span class="video-author-inline">{{video.author.nickname}}</span>
  220. <span class="video-id" *ngIf="video.aweme_id">ID: {{video.aweme_id}}</span>
  221. </div>
  222. <div class="video-interactions">
  223. <span class="interaction-stat like">{{video.statistics.digg_count | numberFormat}}</span>
  224. <span class="interaction-stat comment">{{video.statistics.comment_count | numberFormat}}</span>
  225. <span class="interaction-stat share">{{video.statistics.share_count | numberFormat}}</span>
  226. </div>
  227. </div>
  228. <div class="video-tags" *ngIf="video.text_extra">
  229. <div class="tag-list">
  230. <span class="tag" *ngFor="let tag of video.text_extra">
  231. #{{tag}}
  232. </span>
  233. </div>
  234. </div>
  235. </div>
  236. <div class="card-actions">
  237. <button class="btn" (click)="viewVideoDetail(video)">
  238. <span class="icon">info</span> 查看详情
  239. </button>
  240. <button class="btn" type="button" (click)="analyzeSearchVideo(video)" [disabled]="isAnalyzingViralVideo">
  241. <span class="icon">insights</span> 爆款分析
  242. </button>
  243. <button class="btn btn-primary" (click)="downloadVideo(video)"
  244. [disabled]="video.isDownloading">
  245. <span class="icon">{{ video.isDownloading ? 'hourglass_empty' : 'download' }}</span>
  246. {{ video.isDownloading ? '下载中 ' + video.downloadProgress + '%' : '下载视频' }}
  247. </button>
  248. <button class="btn btn-accent" (click)="startAIGeneration(video)">
  249. <span class="icon">auto_awesome</span> AI重塑
  250. </button>
  251. </div>
  252. @if (video.isDownloading) {
  253. <div class="download-progress">
  254. <div class="progress-bar">
  255. <div class="progress-fill" [style.width]="(video.downloadProgress || 0) + '%'"></div>
  256. </div>
  257. </div>
  258. }
  259. </div>
  260. </div>
  261. <div class="empty-state" *ngIf="filteredSearchResults.length === 0 && !isSearching">
  262. <span class="icon large">filter_alt_off</span>
  263. <h3>没有符合筛选条件的视频</h3>
  264. <p>请调整排序、发布时间或视频时长后再试</p>
  265. </div>
  266. </div>
  267. <div class="empty-state" *ngIf="searchResults.length === 0 && !isSearching">
  268. <span class="icon large">search</span>
  269. <h3>开始搜索抖音视频</h3>
  270. <p>输入关键词发现优质内容,让AI帮您重新创作</p>
  271. </div>
  272. <div class="loading-state" *ngIf="isSearching">
  273. <div class="spinner"></div>
  274. <p>正在搜索相关视频...</p>
  275. </div>
  276. </section>
  277. <section class="dh-page monitor-page monitor-section" *ngIf="currentTab === 'monitor' && auth.isLoggedIn">
  278. <header class="dh-page-header">
  279. <div class="dh-header-icon">
  280. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  281. <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
  282. </svg>
  283. </div>
  284. <div class="dh-header-text">
  285. <h1>博主监测</h1>
  286. <p>通过抖音号 unique_id 添加博主,快速查看主页信息与作品分布</p>
  287. </div>
  288. </header>
  289. <div class="monitor-header monitor-toolbar-row">
  290. <div class="monitor-toolbar">
  291. <div class="form-field search-field">
  292. <label>输入抖音号(unique_id)添加博主</label>
  293. <input type="text" placeholder="例如:TheChief" [(ngModel)]="monitorUniqueIdInput" (keyup.enter)="addMonitorAuthor()">
  294. </div>
  295. <button class="btn btn-primary search-btn" (click)="addMonitorAuthor()" [disabled]="!monitorUniqueIdInput.trim() || isAddingMonitorAuthor">
  296. <span class="icon">{{ isAddingMonitorAuthor ? 'hourglass_empty' : 'person_add' }}</span>
  297. {{ isAddingMonitorAuthor ? '添加中...' : '添加博主' }}
  298. </button>
  299. <button class="btn" type="button" (click)="generateMonitorDailyReport()" [disabled]="isDailyReportGenerating || monitorAuthors.length === 0 || getTotalMonitorWorksCount() === 0">
  300. <span class="icon">summarize</span>
  301. {{isDailyReportGenerating ? '生成中...' : '生成监测日报'}}
  302. </button>
  303. </div>
  304. </div>
  305. <div class="monitor-error" *ngIf="monitorAddError">{{monitorAddError}}</div>
  306. <ng-container *ngIf="!getSelectedMonitorAuthor(); else monitorDetailView">
  307. <div class="monitor-summary" *ngIf="monitorAuthors.length > 0">
  308. <div class="monitor-summary-card">
  309. <span class="monitor-summary-label">已监测博主</span>
  310. <strong>{{monitorAuthors.length}}</strong>
  311. </div>
  312. <div class="monitor-summary-card">
  313. <span class="monitor-summary-label">累计作品</span>
  314. <strong>{{getTotalMonitorWorksCount()}}</strong>
  315. </div>
  316. </div>
  317. <div class="monitor-author-grid" *ngIf="monitorAuthors.length > 0">
  318. <article class="monitor-author-card" *ngFor="let author of monitorAuthors" (click)="openMonitorAuthor(author)">
  319. <div class="monitor-author-top">
  320. <div class="monitor-author-identity">
  321. <img [src]="author.avatarUrl" [alt]="author.nickname" class="monitor-author-avatar" referrerpolicy="no-referrer" (error)="handleMonitorAuthorAvatarError(author)">
  322. <div>
  323. <h3>{{author.nickname}}</h3>
  324. <p class="monitor-author-verified" *ngIf="author.verifiedText">{{author.verifiedText}}</p>
  325. <p class="monitor-author-uid">抖音号:{{author.uniqueId || author.shortId || author.uid}}</p>
  326. </div>
  327. </div>
  328. <div class="monitor-author-actions">
  329. <button class="btn btn-primary" type="button" (click)="$event.stopPropagation(); openMonitorAuthor(author)">
  330. 查看主页
  331. </button>
  332. <button class="btn btn-warn" type="button" (click)="$event.stopPropagation(); removeMonitorAuthor(author)">
  333. 删除
  334. </button>
  335. </div>
  336. </div>
  337. <div class="monitor-author-stats">
  338. <span class="monitor-author-stat">
  339. <span class="monitor-author-stat-label">获赞</span>
  340. <strong class="monitor-author-stat-value">{{author.totalFavorited | numberFormat}}</strong>
  341. </span>
  342. <span class="monitor-author-stat">
  343. <span class="monitor-author-stat-label">粉丝</span>
  344. <strong class="monitor-author-stat-value">{{author.followerCount | numberFormat}}</strong>
  345. </span>
  346. <span class="monitor-author-stat">
  347. <span class="monitor-author-stat-label">作品</span>
  348. <strong class="monitor-author-stat-value">{{author.awemeCount | numberFormat}}</strong>
  349. </span>
  350. </div>
  351. <p class="monitor-author-signature">{{author.signature || '该用户暂未填写简介'}}</p>
  352. <div class="monitor-author-preview" *ngIf="author.works.length > 0">
  353. <div class="monitor-preview-item" *ngFor="let work of author.works.slice(0, 3)">
  354. <img [src]="work.coverUrl" [alt]="work.desc" referrerpolicy="no-referrer" (error)="handleMonitorWorkCoverError(work)">
  355. </div>
  356. </div>
  357. <div class="monitor-author-foot">
  358. <span>{{getMonitorIpLocationText(author)}}</span>
  359. <span *ngIf="author.loadingWorks">作品加载中...</span>
  360. <span *ngIf="!author.loadingWorks">已载入 {{author.works.length}} 条作品</span>
  361. </div>
  362. <span *ngIf="author.lastCheckedAt">上次刷新 {{author.lastCheckedAt | date:'MM-dd HH:mm'}}</span>
  363. <span *ngIf="author.newWorksCount > 0">新增 {{author.newWorksCount}} 条</span>
  364. </article>
  365. </div>
  366. <div class="empty-state" *ngIf="monitorAuthors.length === 0 && !isAddingMonitorAuthor">
  367. <span class="icon large">monitoring</span>
  368. <h3>还没有监测中的博主</h3>
  369. <p>输入抖音号 unique_id 后即可把博主加入监测页,并查看主页作品排布</p>
  370. </div>
  371. </ng-container>
  372. <ng-template #monitorDetailView>
  373. <ng-container *ngIf="getSelectedMonitorAuthor() as selectedAuthor">
  374. <div class="monitor-detail-shell">
  375. <div class="monitor-detail-topbar">
  376. <button class="btn" (click)="backToMonitorList()">
  377. <span class="icon">arrow_back</span> 返回列表
  378. </button>
  379. <div class="monitor-detail-actions">
  380. <button class="btn btn-warn" (click)="removeMonitorAuthor(selectedAuthor)">
  381. <span class="icon">delete</span> 删除博主
  382. </button>
  383. <button class="btn" (click)="reloadMonitorAuthorWorks(selectedAuthor)">
  384. <span class="icon">refresh</span> 刷新作品
  385. </button>
  386. <button class="btn" (click)="setCurrentTab('search')">
  387. <span class="icon">search</span> 去发现页
  388. </button>
  389. </div>
  390. </div>
  391. <div class="monitor-hero" [style.background]="getMonitorHeroBackground(selectedAuthor)">
  392. <div class="monitor-hero-main">
  393. <img [src]="selectedAuthor.avatarUrl" [alt]="selectedAuthor.nickname" class="monitor-hero-avatar" referrerpolicy="no-referrer" (error)="handleMonitorAuthorAvatarError(selectedAuthor)">
  394. <div class="monitor-hero-info">
  395. <div class="monitor-hero-title-row">
  396. <h3>{{selectedAuthor.nickname}}</h3>
  397. <span class="monitor-hero-verified" *ngIf="selectedAuthor.verifiedText">{{selectedAuthor.verifiedText}}</span>
  398. </div>
  399. <div class="monitor-hero-meta">
  400. <span>关注 {{selectedAuthor.followingCount | numberFormat}}</span>
  401. <span>粉丝 {{selectedAuthor.followerCount | numberFormat}}</span>
  402. <span>获赞 {{selectedAuthor.totalFavorited | numberFormat}}</span>
  403. </div>
  404. <div class="monitor-hero-submeta">
  405. <span>抖音号:{{selectedAuthor.uniqueId || selectedAuthor.shortId || selectedAuthor.uid}}</span>
  406. <span>{{getMonitorIpLocationText(selectedAuthor)}}</span>
  407. </div>
  408. <p class="monitor-hero-signature">{{selectedAuthor.signature || '该用户暂未填写简介'}}</p>
  409. </div>
  410. </div>
  411. </div>
  412. <div class="monitor-work-header">
  413. <div>
  414. <h3>作品 {{selectedAuthor.awemeCount | numberFormat}}</h3>
  415. <p>已加载 {{selectedAuthor.works.length}} 条主页作品</p>
  416. </div>
  417. <div class="monitor-work-tools">
  418. <span class="monitor-work-status" *ngIf="selectedAuthor.loadingWorks">正在加载作品...</span>
  419. </div>
  420. </div>
  421. <div class="monitor-works-grid" *ngIf="selectedAuthor.works.length > 0">
  422. <article class="monitor-work-card" *ngFor="let work of selectedAuthor.works">
  423. <div class="monitor-work-cover">
  424. <img [src]="work.coverUrl" [alt]="work.desc" referrerpolicy="no-referrer" (error)="handleMonitorWorkCoverError(work)">
  425. <span class="monitor-work-badge">{{work.type === 'image' ? '图文' : '视频'}}</span>
  426. <span class="monitor-work-duration" *ngIf="work.type === 'video' && work.duration">{{work.duration | timeFormat}}</span>
  427. <span class="monitor-work-badge monitor-work-badge--new" *ngIf="work.isNew">新增</span>
  428. </div>
  429. <div class="monitor-work-content">
  430. <p class="monitor-work-desc">{{work.desc}}</p>
  431. <div class="monitor-work-stats">
  432. <span>点赞 {{work.statistics.digg_count | numberFormat}}</span>
  433. <span>评论 {{work.statistics.comment_count | numberFormat}}</span>
  434. </div>
  435. <div class="monitor-work-date" *ngIf="work.createTime">{{work.createTime | date:'yyyy-MM-dd HH:mm'}}</div>
  436. <div class="monitor-work-actions" *ngIf="work.type === 'video'">
  437. <button class="btn" type="button" (click)="previewMonitorWork(selectedAuthor, work)">
  438. <span class="icon">visibility</span> 查看
  439. </button>
  440. <button class="btn" type="button" (click)="analyzeMonitorWork(selectedAuthor, work)" [disabled]="isAnalyzingViralVideo">
  441. <span class="icon">insights</span> 爆款分析
  442. </button>
  443. <button class="btn btn-primary" type="button" (click)="downloadMonitorWork(selectedAuthor, work)" [disabled]="work.isDownloading">
  444. <span class="icon">{{ work.isDownloading ? 'hourglass_empty' : 'download' }}</span>
  445. {{ work.isDownloading ? '下载中 ' + (work.downloadProgress || 0) + '%' : '下载' }}
  446. </button>
  447. </div>
  448. <div class="monitor-work-progress" *ngIf="work.isDownloading">
  449. <div class="monitor-work-progress-bar">
  450. <div class="monitor-work-progress-fill" [style.width]="(work.downloadProgress || 0) + '%'"></div>
  451. </div>
  452. </div>
  453. </div>
  454. </article>
  455. </div>
  456. <div class="empty-state" *ngIf="selectedAuthor.works.length === 0 && !selectedAuthor.loadingWorks">
  457. <span class="icon large">perm_media</span>
  458. <h3>暂无可展示作品</h3>
  459. <p>暂时没有获取到这个博主的作品,请稍后刷新或更换账号</p>
  460. </div>
  461. <div class="monitor-load-more" *ngIf="selectedAuthor.hasMoreWorks">
  462. <button class="btn btn-primary" (click)="loadMoreMonitorWorks(selectedAuthor)" [disabled]="selectedAuthor.loadingWorks">
  463. <span class="icon">refresh</span>
  464. {{selectedAuthor.loadingWorks ? '加载中...' : '加载更多作品'}}
  465. </button>
  466. </div>
  467. </div>
  468. </ng-container>
  469. </ng-template>
  470. </section>
  471. <app-topic-pool *ngIf="currentTab === 'topic-pool'"
  472. (navigateToPipeline)="onTopicPoolNavigate($event)"
  473. (sourceVideoOpen)="onTopicSourceVideoOpen($event)"
  474. (navigateToBatch)="setCurrentTab('batch-production')">
  475. </app-topic-pool>
  476. <app-ip-operator *ngIf="currentTab === 'ip-operator'"
  477. (navigateToTopicPool)="setCurrentTab('topic-pool')"
  478. (navigateToTopicVideo)="setCurrentTab('topic-to-video')"
  479. (navigateToDigitalHuman)="onIpOperatorDigitalHuman($event)"
  480. (navigateToLibrary)="setCurrentTab('library')">
  481. </app-ip-operator>
  482. <app-analysis-history *ngIf="currentTab === 'analysis-history' && auth.isLoggedIn"
  483. (analysisOpen)="openSavedViralAnalysis($event)">
  484. </app-analysis-history>
  485. <app-daily-report-history *ngIf="currentTab === 'daily-reports' && auth.isLoggedIn"
  486. (reportOpen)="openSavedDailyReport($event)">
  487. </app-daily-report-history>
  488. <!-- 任务管理模块 -->
  489. <section class="dh-page tasks-page" *ngIf="currentTab === 'tasks'">
  490. <header class="dh-page-header">
  491. <div class="dh-header-icon">
  492. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  493. <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
  494. </svg>
  495. </div>
  496. <div class="dh-header-text">
  497. <h1>任务管理</h1>
  498. <p>监控和管理您的 AI 视频生成任务</p>
  499. </div>
  500. </header>
  501. <!-- 顶部统计卡:可点击作为筛选器 -->
  502. <div class="task-stats">
  503. <button type="button" class="task-stat task-stat--pending"
  504. [class.task-stat--active]="tasksFilter === 'pending'"
  505. (click)="toggleTasksFilter('pending')">
  506. <span class="task-stat__icon">
  507. <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  508. <circle cx="12" cy="12" r="10"></circle>
  509. <polyline points="12 6 12 12 16 14"></polyline>
  510. </svg>
  511. </span>
  512. <div class="task-stat__body">
  513. <span class="task-stat__number">{{taskStats.pending}}</span>
  514. <span class="task-stat__label">等待中</span>
  515. </div>
  516. </button>
  517. <button type="button" class="task-stat task-stat--processing"
  518. [class.task-stat--active]="tasksFilter === 'processing'"
  519. (click)="toggleTasksFilter('processing')">
  520. <span class="task-stat__icon">
  521. <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  522. <path d="M21 12a9 9 0 1 1-3-6.7L21 8"></path>
  523. <polyline points="21 3 21 8 16 8"></polyline>
  524. </svg>
  525. </span>
  526. <div class="task-stat__body">
  527. <span class="task-stat__number">{{taskStats.processing}}</span>
  528. <span class="task-stat__label">处理中</span>
  529. </div>
  530. </button>
  531. <button type="button" class="task-stat task-stat--completed"
  532. [class.task-stat--active]="tasksFilter === 'completed'"
  533. (click)="toggleTasksFilter('completed')">
  534. <span class="task-stat__icon">
  535. <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  536. <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
  537. <polyline points="22 4 12 14.01 9 11.01"></polyline>
  538. </svg>
  539. </span>
  540. <div class="task-stat__body">
  541. <span class="task-stat__number">{{taskStats.completed}}</span>
  542. <span class="task-stat__label">已完成</span>
  543. </div>
  544. </button>
  545. <button type="button" class="task-stat task-stat--failed"
  546. [class.task-stat--active]="tasksFilter === 'failed'"
  547. (click)="toggleTasksFilter('failed')">
  548. <span class="task-stat__icon">
  549. <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  550. <circle cx="12" cy="12" r="10"></circle>
  551. <line x1="15" y1="9" x2="9" y2="15"></line>
  552. <line x1="9" y1="9" x2="15" y2="15"></line>
  553. </svg>
  554. </span>
  555. <div class="task-stat__body">
  556. <span class="task-stat__number">{{taskStats.failed}}</span>
  557. <span class="task-stat__label">失败</span>
  558. </div>
  559. </button>
  560. </div>
  561. <section class="task-recovery-audit"
  562. *ngIf="latestRecoveryRun as recoveryRun"
  563. [class.task-recovery-audit--highlight]="isHighlightedRecoveryRun(recoveryRun)">
  564. <div class="task-recovery-audit__main">
  565. <span class="task-recovery-audit__label">最近恢复扫描</span>
  566. <strong>{{formatRecoveryRunTime(recoveryRun.finishedAt)}}</strong>
  567. <span>
  568. 已扫描 {{recoveryRun.summary.scanned}} 个任务,可恢复 {{recoveryRun.summary.recoverable}} 个,完成 {{recoveryRun.summary.completed}} 个,失败 {{recoveryRun.summary.failed}} 个,不可恢复 {{recoveryRun.summary.unrecoverable}} 个
  569. </span>
  570. </div>
  571. <div class="task-recovery-audit__actions">
  572. <span class="task-recovery-audit__id">Run {{recoveryRun.runId}}</span>
  573. <button type="button"
  574. class="task-btn task-btn--ghost"
  575. *ngIf="isHighlightedRecoveryRun(recoveryRun)"
  576. (click)="clearRecoveryRunHighlight()">
  577. 取消高亮
  578. </button>
  579. </div>
  580. </section>
  581. <section class="task-recovery-audit task-recovery-audit--storage"
  582. *ngIf="latestStorageAuditEvent as storageEvent"
  583. [class.task-recovery-audit--highlight]="showStorageAudit">
  584. <div class="task-recovery-audit__main">
  585. <span class="task-recovery-audit__label">存储审计</span>
  586. <strong>最近 {{storageAuditEvents.length}} 条</strong>
  587. <span>
  588. {{storageEvent.namespace}} / {{storageEvent.operation}} / {{storageEvent.status}},{{formatRecoveryRunTime(storageEvent.at)}}
  589. </span>
  590. </div>
  591. <div class="task-recovery-audit__actions">
  592. <span class="task-recovery-audit__id" [title]="storageEvent.message || storageEvent.eventId">
  593. {{storageEvent.storageKey}}
  594. </span>
  595. <button type="button"
  596. class="task-btn task-btn--ghost"
  597. *ngIf="showStorageAudit"
  598. (click)="clearStorageAuditHighlight()">
  599. 取消高亮
  600. </button>
  601. </div>
  602. </section>
  603. <!-- 任务列表 -->
  604. <div class="task-list-section" *ngIf="activeTasks.length > 0">
  605. <div class="task-list-section__head">
  606. <h3 class="task-list-section__title">
  607. 任务列表
  608. <span class="task-list-section__count">{{getFilteredTasks().length}}</span>
  609. </h3>
  610. <button type="button" class="task-btn task-btn--ghost" (click)="exportP0Diagnostics()">
  611. 导出 P0 诊断
  612. </button>
  613. <button type="button" class="task-btn task-btn--ghost" *ngIf="tasksFilter" (click)="toggleTasksFilter(tasksFilter)">
  614. 清除筛选
  615. </button>
  616. </div>
  617. <div class="task-list" *ngIf="getFilteredTasks().length > 0">
  618. <article class="task-row"
  619. *ngFor="let task of getFilteredTasks()"
  620. [class.task-row--processing]="task.status === 'processing'"
  621. [class.task-row--success]="task.status === 'completed'"
  622. [class.task-row--failed]="task.status === 'failed'">
  623. <span class="task-row__accent" aria-hidden="true"></span>
  624. <!-- 头部:标题 + 类型 + 状态 + 时间 -->
  625. <div class="task-row__head">
  626. <div class="task-row__title-wrap">
  627. <span class="task-row__type-badge" *ngIf="task.type === 'video-generation'">视频生成</span>
  628. <h4 class="task-row__title" [title]="task.original_video_title || task.keyword">
  629. {{task.original_video_title || task.keyword || '未命名任务'}}
  630. </h4>
  631. </div>
  632. <div class="task-row__head-right">
  633. <span class="task-row__status-pill"
  634. [class.task-row__status-pill--pending]="task.status === 'pending'"
  635. [class.task-row__status-pill--processing]="task.status === 'processing'"
  636. [class.task-row__status-pill--success]="task.status === 'completed'"
  637. [class.task-row__status-pill--failed]="task.status === 'failed'">
  638. <span class="task-row__status-dot"></span>
  639. {{getTaskStatusText(task.status)}}
  640. </span>
  641. <span class="task-row__date">
  642. <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  643. <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
  644. <line x1="16" y1="2" x2="16" y2="6"></line>
  645. <line x1="8" y1="2" x2="8" y2="6"></line>
  646. <line x1="3" y1="10" x2="21" y2="10"></line>
  647. </svg>
  648. {{task.updated_at | date:'MM-dd HH:mm'}}
  649. </span>
  650. </div>
  651. </div>
  652. <!-- 进度条 -->
  653. <div class="task-row__progress">
  654. <div class="task-row__progress-info">
  655. <span class="task-row__stage">{{getTaskStepText(task.step)}}</span>
  656. <span class="task-row__progress-pct">{{task.progress}}%</span>
  657. </div>
  658. <div class="task-row__progress-bar">
  659. <div class="task-row__progress-fill"
  660. [style.width]="task.progress + '%'"
  661. [class.task-row__progress-fill--success]="task.status === 'completed'"
  662. [class.task-row__progress-fill--failed]="task.status === 'failed'"></div>
  663. </div>
  664. </div>
  665. <!-- 元信息 chips -->
  666. <div class="task-row__meta" *ngIf="task.duration || task.keyword || task.vgData || task.estimatedCredits || task.cost">
  667. <span class="task-row__chip" *ngIf="task.duration">
  668. <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  669. <polygon points="23 7 16 12 23 17 23 7"></polygon>
  670. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
  671. </svg>
  672. {{task.duration}}
  673. </span>
  674. <span class="task-row__chip" *ngIf="task.keyword && (task.original_video_title && task.original_video_title !== task.keyword)">
  675. <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  676. <circle cx="12" cy="12" r="10"></circle>
  677. <circle cx="12" cy="12" r="4"></circle>
  678. </svg>
  679. {{task.keyword}}
  680. </span>
  681. <span class="task-row__chip" *ngIf="task.type === 'video-generation' && task.vgData">
  682. <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  683. <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
  684. <polyline points="2 17 12 22 22 17"></polyline>
  685. <polyline points="2 12 12 17 22 12"></polyline>
  686. </svg>
  687. 步骤 {{task.vgData.step}}/6
  688. </span>
  689. <span class="task-row__chip" *ngIf="task.type === 'video-generation' && task.vgData?.scriptSegments?.length">
  690. <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  691. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  692. <polyline points="14 2 14 8 20 8"></polyline>
  693. </svg>
  694. {{task.vgData?.scriptSegments?.length}} 个片段
  695. </span>
  696. <span class="task-row__chip" *ngIf="task.estimatedCredits || task.cost">
  697. {{getTaskCreditText(task)}}
  698. </span>
  699. </div>
  700. <!-- 错误信息 -->
  701. <div class="task-row__meta" *ngIf="getTaskCostLines(task).length || getTaskExternalIds(task).length || task.refundReason">
  702. <span class="task-row__chip" *ngFor="let line of getTaskCostLines(task)">
  703. {{getTaskCostLineText(line)}}
  704. </span>
  705. <span class="task-row__chip" *ngFor="let item of getTaskExternalIds(task)">
  706. {{item.key}}: {{item.value}}
  707. </span>
  708. <span class="task-row__chip" *ngIf="task.refundReason">
  709. 退款原因:{{task.refundReason}}
  710. </span>
  711. </div>
  712. <div class="task-row__meta" *ngIf="task.steps?.length">
  713. <span class="task-row__chip" *ngFor="let step of task.steps">
  714. {{step.label}} · {{step.status}} · {{step.progress || 0}}%
  715. <ng-container *ngIf="getTaskStepDurationText(step)"> · {{getTaskStepDurationText(step)}}</ng-container>
  716. </span>
  717. </div>
  718. <div class="task-row__error" *ngIf="task.status === 'failed' && task.error_message">
  719. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  720. <circle cx="12" cy="12" r="10"></circle>
  721. <line x1="12" y1="8" x2="12" y2="12"></line>
  722. <line x1="12" y1="16" x2="12.01" y2="16"></line>
  723. </svg>
  724. <span>{{task.error_message}}</span>
  725. </div>
  726. <div class="task-row__error" *ngIf="task.status === 'failed' && (task.failureSuggestion || task.recoverable)">
  727. <span>{{getTaskRecoveryText(task)}}</span>
  728. </div>
  729. <!-- 操作按钮 -->
  730. <div class="task-row__actions">
  731. <a *ngIf="task.status === 'completed' && task.result_url" [href]="task.result_url" target="_blank"
  732. class="task-btn task-btn--success">
  733. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  734. <polygon points="5 3 19 12 5 21 5 3"></polygon>
  735. </svg>
  736. 查看结果
  737. </a>
  738. <button type="button" class="task-btn task-btn--primary"
  739. *ngIf="task.type === 'video-generation' && task.vgData && task.status !== 'completed'"
  740. (click)="vgResumeFromTask(task)">
  741. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  742. <polygon points="5 3 19 12 5 21 5 3"></polygon>
  743. </svg>
  744. 继续任务
  745. </button>
  746. <button type="button" class="task-btn task-btn--primary"
  747. *ngIf="task.type === 'video-generation' && task.vgData && task.status === 'completed'"
  748. (click)="vgResumeFromTask(task)">
  749. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  750. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  751. <circle cx="12" cy="12" r="3"></circle>
  752. </svg>
  753. 查看详情
  754. </button>
  755. <button type="button" class="task-btn task-btn--primary" *ngIf="task.status === 'failed' && (task.retryable || task.recoverable)" (click)="retryTask(task)">
  756. 重试/恢复
  757. </button>
  758. <button type="button" class="task-btn task-btn--ghost" *ngIf="task.status === 'processing'" (click)="cancelTask(task)">
  759. 取消
  760. </button>
  761. <button type="button" class="task-btn task-btn--danger" (click)="removeTask(task.id)" title="删除任务">
  762. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  763. <polyline points="3 6 5 6 21 6"></polyline>
  764. <path d="M19 6l-1.5 14a2 2 0 0 1-2 1.5H8.5a2 2 0 0 1-2-1.5L5 6"></path>
  765. <path d="M10 11v6M14 11v6"></path>
  766. </svg>
  767. </button>
  768. </div>
  769. </article>
  770. </div>
  771. <!-- 筛选后无结果 -->
  772. <div class="task-empty task-empty--inline" *ngIf="getFilteredTasks().length === 0">
  773. <p>当前筛选条件下没有匹配的任务</p>
  774. <button type="button" class="task-btn task-btn--ghost" (click)="toggleTasksFilter(tasksFilter)">
  775. 清除筛选
  776. </button>
  777. </div>
  778. </div>
  779. <div class="task-empty" *ngIf="activeTasks.length === 0">
  780. <div class="task-empty__icon">
  781. <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
  782. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
  783. <polyline points="14 2 14 8 20 8"></polyline>
  784. <line x1="16" y1="13" x2="8" y2="13"></line>
  785. <line x1="16" y1="17" x2="8" y2="17"></line>
  786. <polyline points="10 9 9 9 8 9"></polyline>
  787. </svg>
  788. </div>
  789. <h3>暂无任务</h3>
  790. <p>在视频生成或视频管理中创建任务,任务将显示在这里</p>
  791. </div>
  792. </section>
  793. <!-- 生成历史模块 -->
  794. <section class="dh-page history-page" *ngIf="currentTab === 'history'">
  795. <header class="dh-page-header">
  796. <div class="dh-header-icon">
  797. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  798. <path d="M3 12a9 9 0 1 0 9-9"></path>
  799. <polyline points="3 4 3 9 8 9"></polyline>
  800. <polyline points="12 7 12 12 15 14"></polyline>
  801. </svg>
  802. </div>
  803. <div class="dh-header-text">
  804. <h1>生成历史</h1>
  805. <p>所有 AI 生成任务的完整记录</p>
  806. </div>
  807. </header>
  808. <div class="history-toolbar">
  809. <div class="history-toolbar__filter" role="tablist">
  810. <button type="button" class="history-pill"
  811. [class.history-pill--active]="!historyFilter.status"
  812. (click)="historyFilter.status = ''">
  813. 全部 <span class="history-pill__count">{{getHistoryStatusCount('')}}</span>
  814. </button>
  815. <button type="button" class="history-pill history-pill--success"
  816. [class.history-pill--active]="historyFilter.status === 'completed'"
  817. (click)="historyFilter.status = 'completed'">
  818. 已完成 <span class="history-pill__count">{{getHistoryStatusCount('completed')}}</span>
  819. </button>
  820. <button type="button" class="history-pill history-pill--danger"
  821. [class.history-pill--active]="historyFilter.status === 'failed'"
  822. (click)="historyFilter.status = 'failed'">
  823. 失败 <span class="history-pill__count">{{getHistoryStatusCount('failed')}}</span>
  824. </button>
  825. </div>
  826. <button type="button" class="history-export" (click)="exportHistory()" title="导出全部记录为 JSON">
  827. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  828. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  829. <polyline points="7 10 12 15 17 10"></polyline>
  830. <line x1="12" y1="15" x2="12" y2="3"></line>
  831. </svg>
  832. <span>导出记录</span>
  833. </button>
  834. </div>
  835. <div class="history-list" *ngIf="getFilteredHistoryRecords().length > 0">
  836. <ng-container *ngFor="let record of getFilteredHistoryRecords()">
  837. <article class="history-card"
  838. [class.history-card--success]="record.status === 'completed'"
  839. [class.history-card--failed]="record.status === 'failed'"
  840. *ngIf="parseHistoryRecordTitle(record) as parsed">
  841. <span class="history-card__accent" aria-hidden="true"></span>
  842. <div class="history-card__body">
  843. <div class="history-card__head">
  844. <h4 class="history-card__title">{{parsed.title}}</h4>
  845. <span class="history-card__date">
  846. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  847. <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
  848. <line x1="16" y1="2" x2="16" y2="6"></line>
  849. <line x1="8" y1="2" x2="8" y2="6"></line>
  850. <line x1="3" y1="10" x2="21" y2="10"></line>
  851. </svg>
  852. {{record.created_at | date:'yyyy-MM-dd HH:mm'}}
  853. </span>
  854. </div>
  855. <div class="history-card__tags" *ngIf="parsed.tags.length > 0">
  856. <span class="history-tag" *ngFor="let t of parsed.tags">#{{t}}</span>
  857. </div>
  858. <div class="history-card__meta">
  859. <span class="history-status-pill"
  860. [class.history-status-pill--success]="record.status === 'completed'"
  861. [class.history-status-pill--failed]="record.status === 'failed'">
  862. <span class="history-status-pill__dot"></span>
  863. {{getStatusText(record.status)}}
  864. </span>
  865. <span class="history-chip" *ngIf="record.styleName">风格 · {{record.styleName}}</span>
  866. <span class="history-chip" *ngIf="record.successCount !== undefined">片段 · {{record.successCount}}/{{record.segmentCount}}</span>
  867. <span class="history-chip" *ngIf="record.audioSourceLabel">音频 · {{record.audioSourceLabel}}</span>
  868. <span class="history-chip" *ngIf="record.duration">{{record.duration}}</span>
  869. </div>
  870. </div>
  871. <div class="history-card__cost">
  872. <span class="history-card__cost-label">费用</span>
  873. <span class="history-card__cost-value">¥{{record.cost || 0}}</span>
  874. </div>
  875. <div class="history-card__actions">
  876. <button type="button" class="history-btn history-btn--primary" (click)="viewHistoryDetail(record)">
  877. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  878. <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
  879. <circle cx="12" cy="12" r="3"></circle>
  880. </svg>
  881. 查看详情
  882. </button>
  883. <button type="button" class="history-btn history-btn--ghost" (click)="removeHistoryRecord(record.id)" title="删除该条记录">
  884. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  885. <polyline points="3 6 5 6 21 6"></polyline>
  886. <path d="M19 6l-1.5 14a2 2 0 0 1-2 1.5H8.5a2 2 0 0 1-2-1.5L5 6"></path>
  887. <path d="M10 11v6M14 11v6"></path>
  888. <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path>
  889. </svg>
  890. </button>
  891. </div>
  892. </article>
  893. </ng-container>
  894. </div>
  895. <div class="history-empty" *ngIf="getFilteredHistoryRecords().length === 0">
  896. <div class="history-empty__icon">
  897. <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
  898. <path d="M3 12a9 9 0 1 0 9-9"></path>
  899. <polyline points="3 4 3 9 8 9"></polyline>
  900. <polyline points="12 7 12 12 15 14"></polyline>
  901. </svg>
  902. </div>
  903. <h3>{{historyRecords.length === 0 ? '暂无历史记录' : '没有符合筛选条件的记录'}}</h3>
  904. <p>{{historyRecords.length === 0 ? '完成第一个 AI 生成任务后,记录将显示在这里' : '试试切换其他状态标签,或清除当前筛选'}}</p>
  905. <button type="button" class="history-btn history-btn--ghost" *ngIf="historyRecords.length > 0 && historyFilter.status" (click)="historyFilter.status = ''">
  906. 清除筛选
  907. </button>
  908. </div>
  909. </section>
  910. <!-- 结果展示模块 -->
  911. <section class="dh-page results-page" *ngIf="currentTab === 'results'">
  912. <header class="dh-page-header">
  913. <div class="dh-header-icon">
  914. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  915. <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
  916. <polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none"></polygon>
  917. </svg>
  918. </div>
  919. <div class="dh-header-text">
  920. <h1>结果展示</h1>
  921. <p>查看和下载您的 AI 生成内容</p>
  922. </div>
  923. </header>
  924. <div class="results-grid" *ngIf="generatedResults.length > 0">
  925. <div class="result-card" *ngFor="let result of generatedResults">
  926. <div class="result-media">
  927. <div class="video-preview" *ngIf="result.type === 'video' && result.url">
  928. <video controls [src]="result.url" preload="metadata"></video>
  929. <div class="result-badge">
  930. <span class="icon">play_circle</span>
  931. <span>{{result.quality || '视频'}}</span>
  932. </div>
  933. </div>
  934. <div class="image-preview" *ngIf="result.type === 'image'">
  935. <img [src]="result.url" [alt]="result.title">
  936. <div class="result-badge">
  937. <span class="icon">image</span>
  938. <span>图片</span>
  939. </div>
  940. </div>
  941. <div *ngIf="result.type === 'video' && !result.url" style="padding:24px;text-align:center;color:var(--text-secondary);background:var(--bg-tertiary);border-radius:8px">
  942. <span class="icon" style="font-size:36px;display:block;margin-bottom:8px">videocam_off</span>
  943. <span>无主视频(查看下方片段)</span>
  944. </div>
  945. </div>
  946. <div class="result-content">
  947. <h4 class="result-title">{{result.title}}</h4>
  948. <div class="result-meta">
  949. <span class="result-date">{{result.created_at | date:'yyyy-MM-dd HH:mm'}}</span>
  950. <span class="result-quality">{{result.quality}}</span>
  951. </div>
  952. <div class="result-stats">
  953. <span class="stat-item">
  954. <span class="icon">video_library</span>
  955. <span>{{result.duration}}</span>
  956. </span>
  957. <span class="stat-item">
  958. <span class="icon">attach_money</span>
  959. <span>费用: ¥{{result.cost}}</span>
  960. </span>
  961. </div>
  962. <!-- 片段视频列表 -->
  963. <div *ngIf="result.segments && result.segments.length > 0" style="margin-top:10px">
  964. <details>
  965. <summary style="cursor:pointer;font-size:13px;color:var(--accent-color);user-select:none">
  966. <span class="icon" style="font-size:14px;vertical-align:middle">expand_more</span>
  967. 展开全部 {{result.segments.length}} 个片段
  968. </summary>
  969. <div style="margin-top:8px;display:flex;flex-direction:column;gap:8px">
  970. <div *ngFor="let seg of result.segments" style="padding:8px;background:var(--bg-tertiary);border-radius:6px">
  971. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
  972. <span style="font-size:12px;font-weight:600">{{seg.id}}</span>
  973. <span class="status-badge badge-success" style="font-size:11px">成功</span>
  974. </div>
  975. <video controls [src]="seg.videoUrl" preload="metadata" style="width:100%;max-height:200px;border-radius:4px;background:#000"></video>
  976. <a [href]="seg.videoUrl" target="_blank" style="display:inline-block;margin-top:4px;font-size:12px;color:var(--accent-color)">
  977. <span class="icon" style="font-size:12px">open_in_new</span> 新窗口
  978. </a>
  979. </div>
  980. </div>
  981. </details>
  982. </div>
  983. </div>
  984. <div class="result-actions">
  985. <button class="btn" (click)="previewResult(result)" *ngIf="result.url">
  986. <span class="icon">visibility</span> 预览
  987. </button>
  988. <button class="btn btn-primary" (click)="downloadResult(result)" *ngIf="result.url">
  989. <span class="icon">download</span> 下载
  990. </button>
  991. <button class="btn" (click)="removeResult(result.id)">
  992. <span class="icon">delete</span> 删除
  993. </button>
  994. </div>
  995. </div>
  996. </div>
  997. <div class="empty-state" *ngIf="generatedResults.length === 0">
  998. <span class="icon large">video_library</span>
  999. <h3>暂无生成结果</h3>
  1000. <p>完成AI生成任务后,结果将显示在这里</p>
  1001. </div>
  1002. </section>
  1003. <!-- 视频管理模块 -->
  1004. <section class="dh-page videos-page" *ngIf="currentTab === 'videos' && auth.isLoggedIn">
  1005. <header class="dh-page-header">
  1006. <div class="dh-header-icon">
  1007. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1008. <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
  1009. </svg>
  1010. </div>
  1011. <div class="dh-header-text">
  1012. <h1>视频管理</h1>
  1013. <p>本地已下载与生成的视频素材库</p>
  1014. </div>
  1015. </header>
  1016. <div class="videos-header videos-toolbar-row">
  1017. <div class="videos-controls">
  1018. <div class="form-field filter-field">
  1019. <label>分类筛选</label>
  1020. <select [(ngModel)]="videoFilter.category">
  1021. <option value="">全部分类</option>
  1022. <option *ngFor="let category of videoCategories" [value]="category.id">
  1023. {{category.name}} ({{category.count}})
  1024. </option>
  1025. </select>
  1026. </div>
  1027. <div class="form-field filter-field">
  1028. <label>来源</label>
  1029. <select [(ngModel)]="videoFilter.source">
  1030. <option value="">全部来源</option>
  1031. <option value="downloaded">下载</option>
  1032. <option value="generated">AI生成</option>
  1033. <option value="uploaded">上传</option>
  1034. </select>
  1035. </div>
  1036. <div class="form-field search-field">
  1037. <label>搜索视频</label>
  1038. <input type="text" placeholder="输入关键词搜索..." [(ngModel)]="videoFilter.searchTerm">
  1039. </div>
  1040. <button class="btn btn-primary" type="button" (click)="openUploadModal()" style="position:relative;z-index:10">
  1041. <span class="icon">upload</span> 上传视频
  1042. </button>
  1043. <button class="btn" (click)="isSelectionMode = !isSelectionMode">
  1044. <span class="icon">{{isSelectionMode ? 'close' : 'check_circle'}}</span>
  1045. {{isSelectionMode ? '取消选择' : '批量操作'}}
  1046. </button>
  1047. </div>
  1048. </div>
  1049. <div class="videos-actions" *ngIf="isSelectionMode && selectedVideos.size > 0">
  1050. <div class="selection-info">
  1051. 已选择 {{selectedVideos.size}} 个视频
  1052. </div>
  1053. <div class="batch-actions">
  1054. <button class="btn" (click)="batchDelete()">
  1055. <span class="icon">delete</span> 批量删除
  1056. </button>
  1057. <button class="btn" (click)="batchExport()">
  1058. <span class="icon">download</span> 批量导出
  1059. </button>
  1060. <button class="btn" (click)="batchCategorize()">
  1061. <span class="icon">folder</span> 批量分类
  1062. </button>
  1063. </div>
  1064. </div>
  1065. <div class="videos-grid" *ngIf="getFilteredVideos().length > 0">
  1066. <div class="video-item" *ngFor="let video of getFilteredVideos()" [class.selected]="selectedVideos.has(video.id)">
  1067. <div class="video-checkbox" *ngIf="isSelectionMode">
  1068. <input type="checkbox" [checked]="selectedVideos.has(video.id)" (change)="toggleVideoSelection(video.id)">
  1069. </div>
  1070. <div class="video-thumbnail">
  1071. <img *ngIf="video.thumbnail" [src]="video.thumbnail" [alt]="video.title">
  1072. <video *ngIf="!video.thumbnail && getVideoPlayUrl(video)" [src]="getVideoPlayUrl(video) + '#t=0.1'" muted preload="metadata" style="width:100%;height:100%;object-fit:cover;pointer-events:none"></video>
  1073. <div *ngIf="!video.thumbnail && !getVideoPlayUrl(video)" class="video-thumb-placeholder">
  1074. <span class="icon" style="font-size:36px;color:var(--text-muted)">movie</span>
  1075. </div>
  1076. <div class="video-duration">{{video.duration | timeFormat}}</div>
  1077. <div class="video-source" [class]="video.source">{{getSourceText(video.source)}}</div>
  1078. <!-- AI重塑进度指示器 -->
  1079. <div class="remix-progress" *ngIf="video.isRemixing">
  1080. <div class="remix-progress-bar">
  1081. <div class="remix-progress-fill" [style.width]="video.remixProgress + '%'"></div>
  1082. </div>
  1083. <div class="remix-style">{{video.remixStyle}}</div>
  1084. </div>
  1085. </div>
  1086. <div class="video-info">
  1087. <div class="video-title-row">
  1088. <h4 class="video-title">{{video.title}}</h4>
  1089. <span class="category-badge" [style.background-color]="getCategoryColor(video.category)">
  1090. {{getCategoryName(video.category)}}
  1091. </span>
  1092. </div>
  1093. <div class="video-meta">
  1094. <span class="meta-item">
  1095. <span class="icon">schedule</span>
  1096. {{video.created_at | date:'yyyy-MM-dd HH:mm'}}
  1097. </span>
  1098. <span class="meta-item">
  1099. <span class="icon">storage</span>
  1100. {{video.size | fileSize}}
  1101. </span>
  1102. <span class="meta-item">
  1103. <span class="icon">hd</span>
  1104. {{video.metadata.resolution}}
  1105. </span>
  1106. </div>
  1107. <div class="video-description" *ngIf="video.description">
  1108. {{video.description}}
  1109. </div>
  1110. <div class="video-tags" *ngIf="video.tags.length > 0">
  1111. <span class="tag" *ngFor="let tag of video.tags">
  1112. #{{tag}}
  1113. </span>
  1114. </div>
  1115. </div>
  1116. <div class="video-actions">
  1117. <button class="btn" (click)="previewVideo(video)">
  1118. <span class="icon">play_arrow</span> 预览
  1119. </button>
  1120. <button class="btn" (click)="editVideo(video)">
  1121. <span class="icon">edit</span> 编辑
  1122. </button>
  1123. <button class="btn {{getRemixStatusColor(video)}}" (click)="openRemixModeChooser(video)" [disabled]="video.isRemixing">
  1124. <span class="icon">
  1125. {{video.isRemixing ? 'hourglass_empty' : 'auto_awesome'}}
  1126. </span>
  1127. {{getRemixStatusText(video)}}
  1128. </button>
  1129. <button class="btn" (click)="viewRemixVersions(video)" *ngIf="video.remixVersions && video.remixVersions.length > 0">
  1130. <span class="icon">history</span> 版本
  1131. </button>
  1132. <button class="btn btn-warn" (click)="deleteVideo(video)">
  1133. <span class="icon">delete</span> 删除
  1134. </button>
  1135. </div>
  1136. </div>
  1137. </div>
  1138. <div class="empty-state" *ngIf="getFilteredVideos().length === 0">
  1139. <span class="icon large">folder_open</span>
  1140. <h3>暂无视频文件</h3>
  1141. <p>下载、生成或上传视频后,它们将显示在这里进行管理</p>
  1142. <button class="btn btn-primary" style="margin-top:12px" (click)="openUploadModal()">
  1143. <span class="icon">upload</span> 上传视频
  1144. </button>
  1145. </div>
  1146. </section>
  1147. <!-- 语音合成模块(重设计:二栏布局) -->
  1148. <section class="dh-page vs-page" *ngIf="currentTab === 'voice-synthesis'">
  1149. <header class="dh-page-header">
  1150. <div class="dh-header-icon">
  1151. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1152. <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
  1153. <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
  1154. <line x1="12" y1="19" x2="12" y2="23"></line>
  1155. <line x1="8" y1="23" x2="16" y2="23"></line>
  1156. </svg>
  1157. </div>
  1158. <div class="dh-header-text">
  1159. <h1>声音定制</h1>
  1160. <p>上传音频样本训练专属声音模型,或选择已有声音模型生成语音</p>
  1161. </div>
  1162. <button type="button" class="dh-help-btn" (click)="showToast('先在左侧「声音训练」上传样本完成训练,再到右侧「声音合成」选择声音模型生成语音。', 'info', 5000)">
  1163. <span class="dh-help-icon">?</span>
  1164. <span>帮助</span>
  1165. </button>
  1166. </header>
  1167. <div class="vs-two-col">
  1168. <!-- ========== 左:声音训练 ========== -->
  1169. <article class="dh-card vs-subcard">
  1170. <header class="vs-subcard-header">
  1171. <div class="vs-subcard-icon vs-subcard-icon--purple">
  1172. <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1173. <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
  1174. <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
  1175. <line x1="12" y1="19" x2="12" y2="23"></line>
  1176. <line x1="8" y1="23" x2="16" y2="23"></line>
  1177. </svg>
  1178. </div>
  1179. <div>
  1180. <h2>声音训练</h2>
  1181. <p>上传音频样本,训练专属声音模型</p>
  1182. </div>
  1183. <button type="button" class="dh-help-btn">
  1184. <span class="dh-help-icon">?</span>
  1185. <span>帮助</span>
  1186. </button>
  1187. </header>
  1188. <div class="dh-segment vs-tab-header">
  1189. <button type="button" class="dh-segment__item is-active">上传音频</button>
  1190. <button type="button" class="dh-segment__item" disabled title="即将上线">直接录音</button>
  1191. </div>
  1192. <input type="file" id="vsCloneAudioInput"
  1193. accept=".mp3,.wav,.m4a,.ogg,.aac,.flac,.wma,audio/*"
  1194. (change)="onVsCloneAudioSelected($event)"
  1195. hidden
  1196. [disabled]="vsCloneLoading || vsCloneUploading">
  1197. <label for="vsCloneAudioInput" class="dh-upload__zone vs-audio-upload" *ngIf="!vsCloneAudioName">
  1198. <svg class="dh-upload__icon" viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
  1199. <path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
  1200. <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
  1201. </svg>
  1202. <h3 class="dh-upload__title">点击或拖拽音频文件到此区域</h3>
  1203. <ul class="dh-upload__hints">
  1204. <li>支持 MP3 / WAV / M4A / OGG / AAC / FLAC 等格式</li>
  1205. <li>建议时长 5-30 秒,文件大小不超过 50MB</li>
  1206. <li>单人纯净人声、无背景音乐和噪声效果最佳</li>
  1207. </ul>
  1208. </label>
  1209. <div class="vs-upload-filled" *ngIf="vsCloneAudioName">
  1210. <div style="min-width:0;flex:1">
  1211. <div class="dh-upload__preview-name">{{vsCloneAudioName}}</div>
  1212. <div class="dh-upload__preview-size">
  1213. {{vsCloneAudioFile ? (vsCloneAudioFile.size / 1024 / 1024).toFixed(2) + ' MB' : ''}}
  1214. <span *ngIf="vsCloneAudioUrl" style="color:var(--dh-success)"> · 已上传</span>
  1215. <span *ngIf="vsCloneUploading" style="color:#c4b5fd"> · 正在上传...</span>
  1216. </div>
  1217. </div>
  1218. <div class="dh-upload__preview-actions" *ngIf="!vsCloneLoading && !vsCloneUploading">
  1219. <label for="vsCloneAudioInput" class="dh-chip-btn">更换</label>
  1220. <button type="button" class="dh-chip-btn dh-chip-btn--danger" (click)="clearVsCloneAudio()">移除</button>
  1221. </div>
  1222. </div>
  1223. <div class="dh-field">
  1224. <label class="dh-field__label">
  1225. <span>声音名称 <em>*</em></span>
  1226. <span class="dh-field__counter">{{(vsCloneForm.name || '').length}} / 20</span>
  1227. </label>
  1228. <input class="dh-input" type="text" maxlength="20" [(ngModel)]="vsCloneForm.name"
  1229. placeholder="为这个声音起个名字"
  1230. [disabled]="vsCloneLoading || vsCloneUploading">
  1231. <p class="vs-hint">起个名字方便记忆(20 字以内),例如:我的声音</p>
  1232. </div>
  1233. <div class="dh-field-row">
  1234. <div class="dh-field" style="flex:1;min-width:0">
  1235. <label class="dh-field__label"><span>语言</span></label>
  1236. <select class="dh-input" [(ngModel)]="vsCloneForm.language" [disabled]="vsCloneLoading || vsCloneUploading">
  1237. <option [ngValue]="0">中文</option>
  1238. <option [ngValue]="1">英文</option>
  1239. </select>
  1240. <p class="vs-hint">系统会自动准备声音训练所需信息,无需手动配置</p>
  1241. </div>
  1242. </div>
  1243. <div class="dh-field">
  1244. <label class="dh-field__label"><span>音频对应文本(可选)</span></label>
  1245. <input class="dh-input" type="text" [(ngModel)]="vsCloneForm.audioText"
  1246. placeholder="音频中说的文本内容,有助于提升训练质量"
  1247. [disabled]="vsCloneLoading || vsCloneUploading">
  1248. </div>
  1249. <div class="dh-field">
  1250. <label class="dh-field__label"><span>试听文本(4-80 字) <em>*</em></span></label>
  1251. <input class="dh-input" type="text" [(ngModel)]="vsCloneForm.demoText"
  1252. placeholder="训练成功后用于生成试听音频的文本"
  1253. [disabled]="vsCloneLoading || vsCloneUploading">
  1254. </div>
  1255. <div class="dh-error" *ngIf="vsCloneError">{{vsCloneError}}</div>
  1256. <button type="button" class="dh-cta" (click)="vsStartClone()" [disabled]="vsCloneLoading || vsCloneUploading">
  1257. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1258. <polygon points="5 3 19 12 5 21 5 3"></polygon>
  1259. </svg>
  1260. <span>{{vsCloneUploading ? '上传音频中...' : vsCloneLoading ? '训练中...' : '开始训练'}}</span>
  1261. </button>
  1262. <p class="vs-hint" style="text-align:center"
  1263. *ngIf="!vsCloneLoading && !vsCloneUploading && (!vsCloneForm.name || !vsCloneAudioName)">
  1264. {{ !vsCloneForm.name ? '请输入声音名称' : '请上传或录制音频' }}
  1265. </p>
  1266. <!-- 训练结果 -->
  1267. <div class="vs-result" *ngIf="vsCloneResult?.code === 200 && vsCloneResult?.data?.timbre">
  1268. <div class="vs-result-head">
  1269. <span class="dh-status-banner__check">✓</span>
  1270. <span>训练成功</span>
  1271. </div>
  1272. <div class="vs-result-info">
  1273. <div><strong>声音名称:</strong>{{vsCloneResult.data.timbre.name || vsCloneForm.name}}</div>
  1274. <div *ngIf="vsCloneResult.data.timbre.models?.[0]?.demo_audio">
  1275. <strong>试听:</strong>
  1276. <audio controls [src]="vsCloneResult.data.timbre.models[0].demo_audio" preload="metadata" class="vs-audio-compact"></audio>
  1277. </div>
  1278. </div>
  1279. <button type="button" class="dh-link-btn" style="margin-top:10px"
  1280. (click)="vsSelectClonedTimbre(vsCloneResult.data.timbre.objectId)">
  1281. 使用此声音生成语音 →
  1282. </button>
  1283. </div>
  1284. <!-- 已训练快捷列表 -->
  1285. <div class="vs-cloned-list" *ngIf="vsClonedTimbreList.length > 0">
  1286. <h4 class="vs-subtle-title">我的声音模型 ({{vsClonedTimbreList.length}})</h4>
  1287. <div class="dh-chips">
  1288. <button type="button" class="dh-chip" *ngFor="let t of vsClonedTimbreList"
  1289. (click)="vsSelectClonedTimbre(t.objectId)">
  1290. {{t.name}}
  1291. </button>
  1292. </div>
  1293. </div>
  1294. </article>
  1295. <!-- ========== 右:声音合成 ========== -->
  1296. <article class="dh-card vs-subcard">
  1297. <header class="vs-subcard-header">
  1298. <div class="vs-subcard-icon vs-subcard-icon--pink">
  1299. <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1300. <path d="M3 10v3M7 6v11M11 3v17M15 7v9M19 10v3"></path>
  1301. </svg>
  1302. </div>
  1303. <div>
  1304. <h2>声音合成</h2>
  1305. <p>选择声音模型,生成个性化语音</p>
  1306. </div>
  1307. <button type="button" class="dh-help-btn">
  1308. <span class="dh-help-icon">?</span>
  1309. <span>帮助</span>
  1310. </button>
  1311. </header>
  1312. <div class="dh-field">
  1313. <label class="dh-field__label"><span>选择声音模型</span></label>
  1314. <button type="button" class="vs-voice-picker-trigger"
  1315. (click)="openVoicePicker()"
  1316. [disabled]="vsSynthLoading"
  1317. [class.is-empty]="!currentVoiceSelection">
  1318. <ng-container *ngIf="currentVoiceSelection; else emptyVoiceTpl">
  1319. <span class="vs-voice-picker-avatar"
  1320. [class.vs-voice-picker-avatar--mine]="currentVoiceSelection.source === 'cloned'">
  1321. {{currentVoiceSelection.name.charAt(0)}}
  1322. </span>
  1323. <span class="vs-voice-picker-info">
  1324. <span class="vs-voice-picker-name">{{currentVoiceSelection.name}}</span>
  1325. <span class="vs-voice-picker-src">
  1326. {{currentVoiceSelection.source === 'official' ? '声音库' : '我的声音'}}
  1327. </span>
  1328. </span>
  1329. </ng-container>
  1330. <ng-template #emptyVoiceTpl>
  1331. <span class="vs-voice-picker-avatar vs-voice-picker-avatar--empty">+</span>
  1332. <span class="vs-voice-picker-info">
  1333. <span class="vs-voice-picker-name">请选择声音模型</span>
  1334. <span class="vs-voice-picker-src">点击选择声音库 / 我的声音</span>
  1335. </span>
  1336. </ng-template>
  1337. <span class="vs-voice-picker-chevron">›</span>
  1338. </button>
  1339. </div>
  1340. <app-voice-picker
  1341. [open]="voicePickerOpen"
  1342. [clonedList]="vsClonedTimbreList"
  1343. [currentSelection]="currentVoiceSelection"
  1344. (select)="onVoicePicked($event)"
  1345. (close)="closeVoicePicker()">
  1346. </app-voice-picker>
  1347. <div class="dh-field">
  1348. <label class="dh-field__label">
  1349. <span>输入文本</span>
  1350. <span class="dh-field__counter">{{(vsSynthForm.text || '').length}} / 5000</span>
  1351. </label>
  1352. <textarea class="dh-textarea" rows="4" maxlength="5000" [(ngModel)]="vsSynthForm.text"
  1353. placeholder="你好,这是一个声音克隆测试。"
  1354. [disabled]="vsSynthLoading"></textarea>
  1355. </div>
  1356. <div class="dh-field-row">
  1357. <div class="dh-field" style="flex:1;min-width:0">
  1358. <label class="dh-field__label">
  1359. <span>语速</span>
  1360. <span class="dh-field__counter">{{vsSynthForm.speechRate}}</span>
  1361. </label>
  1362. <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.speechRate" [disabled]="vsSynthLoading">
  1363. </div>
  1364. <div class="dh-field" style="flex:1;min-width:0">
  1365. <label class="dh-field__label">
  1366. <span>音量</span>
  1367. <span class="dh-field__counter">{{vsSynthForm.loudnessRate}}</span>
  1368. </label>
  1369. <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.loudnessRate" [disabled]="vsSynthLoading">
  1370. </div>
  1371. </div>
  1372. <div class="dh-field-row">
  1373. <div class="dh-field" style="flex:1;min-width:0">
  1374. <label class="dh-field__label"><span>音频格式</span></label>
  1375. <select class="dh-input" [(ngModel)]="vsSynthForm.format" [disabled]="vsSynthLoading">
  1376. <option value="mp3">MP3</option>
  1377. <option value="ogg_opus">OGG Opus</option>
  1378. <option value="pcm">PCM</option>
  1379. </select>
  1380. </div>
  1381. <div class="dh-field" style="flex:1;min-width:0">
  1382. <label class="dh-field__label"><span>采样率</span></label>
  1383. <select class="dh-input" [(ngModel)]="vsSynthForm.sampleRate" [disabled]="vsSynthLoading">
  1384. <option [ngValue]="8000">8000 Hz</option>
  1385. <option [ngValue]="16000">16000 Hz</option>
  1386. <option [ngValue]="24000">24000 Hz</option>
  1387. <option [ngValue]="48000">48000 Hz</option>
  1388. </select>
  1389. </div>
  1390. </div>
  1391. <div class="dh-error" *ngIf="vsSynthError">{{vsSynthError}}</div>
  1392. <button type="button" class="dh-cta" (click)="vsStartSynthesize()" [disabled]="vsSynthLoading">
  1393. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  1394. <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
  1395. <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
  1396. </svg>
  1397. <span>{{vsSynthLoading ? '合成中...' : '生成语音'}}</span>
  1398. </button>
  1399. <p class="vs-hint" style="text-align:center"
  1400. *ngIf="!vsSynthLoading && (!currentVoiceSelection || !vsSynthForm.text)">
  1401. {{ !currentVoiceSelection ? '请选择声音模型' : '请输入要合成的文本' }}
  1402. </p>
  1403. <!-- 合成结果 -->
  1404. <div class="vs-result" *ngIf="vsSynthAudioUrl">
  1405. <div class="vs-result-head">
  1406. <span class="dh-status-banner__check">✓</span>
  1407. <span>合成成功</span>
  1408. </div>
  1409. <audio controls [src]="vsSynthAudioUrl" preload="metadata" class="vs-audio-full"></audio>
  1410. <div class="vs-result-info">
  1411. <div *ngIf="vsSynthWorkId"><strong>生成记录:</strong>已保存</div>
  1412. </div>
  1413. <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
  1414. <a [href]="vsSynthAudioUrl" target="_blank" class="dh-link-btn dh-link-btn--ghost">新窗口播放</a>
  1415. <a [href]="vsSynthAudioUrl" download class="dh-link-btn">下载音频</a>
  1416. </div>
  1417. </div>
  1418. </article>
  1419. </div>
  1420. </section>
  1421. <!-- LEGACY 语音合成(旧版 *ngIf="false" 死代码,保留作回滚备份) -->
  1422. <section class="section-card" *ngIf="false">
  1423. <div class="search-header">
  1424. <h2>
  1425. <span class="icon">record_voice_over</span> 语音合成
  1426. </h2>
  1427. <p class="subtitle">训练自己的声音模型,或选择已有声音生成自然语音</p>
  1428. </div>
  1429. <!-- 模式切换 -->
  1430. <div class="remix-mode-grid" style="margin-bottom:20px">
  1431. <div class="remix-mode-card"
  1432. [class.active]="vsTtsMode === 'clone'"
  1433. (click)="setVsTtsMode('clone')">
  1434. <div class="remix-mode-title">音色复刻</div>
  1435. <div class="remix-mode-desc">上传音频样本,训练可重复使用的声音模型。</div>
  1436. </div>
  1437. <div class="remix-mode-card"
  1438. [class.active]="vsTtsMode === 'synthesize'"
  1439. (click)="setVsTtsMode('synthesize')">
  1440. <div class="remix-mode-title">语音合成</div>
  1441. <div class="remix-mode-desc">选择声音模型,将文本内容转为语音音频。</div>
  1442. </div>
  1443. </div>
  1444. <!-- 音色复刻表单 -->
  1445. <div *ngIf="vsTtsMode === 'clone'" style="display:flex;flex-direction:column;gap:14px">
  1446. <div class="form-field">
  1447. <label>音色名称 *</label>
  1448. <input type="text" [(ngModel)]="vsCloneForm.name" placeholder="例如:我的专属音色" [disabled]="vsCloneLoading || vsCloneUploading">
  1449. </div>
  1450. <div class="form-field">
  1451. <label>训练通道 *</label>
  1452. <select [(ngModel)]="vsCloneForm.speakerId" [disabled]="vsCloneLoading || vsCloneUploading">
  1453. <option value="S_ULZKhZWZ1">S_ULZKhZWZ1</option>
  1454. <option value="S_TLZKhZWZ1">S_TLZKhZWZ1</option>
  1455. <option value="S_SLZKhZWZ1">S_SLZKhZWZ1</option>
  1456. <option value="S_QLZKhZWZ1">S_QLZKhZWZ1</option>
  1457. <option value="S_PLZKhZWZ1">S_PLZKhZWZ1</option>
  1458. <option value="S_OLZKhZWZ1">S_OLZKhZWZ1</option>
  1459. <option value="S_MLZKhZWZ1">S_MLZKhZWZ1</option>
  1460. <option value="S_RWDjSzXZ1">S_RWDjSzXZ1</option>
  1461. </select>
  1462. </div>
  1463. <div class="form-field">
  1464. <label>上传音频文件 *</label>
  1465. <input type="file" id="vsCloneAudioInput"
  1466. accept=".mp3,.wav,.m4a,.ogg,.aac,.flac,.wma,audio/*"
  1467. (change)="onVsCloneAudioSelected($event)"
  1468. style="display:none"
  1469. [disabled]="vsCloneLoading || vsCloneUploading">
  1470. <div style="border:2px dashed var(--border-color);border-radius:10px;padding:20px;text-align:center;transition:border-color 0.2s ease,background-color 0.2s ease"
  1471. [style.border-color]="vsCloneAudioName ? 'var(--accent-color)' : 'var(--border-color)'"
  1472. [style.background]="vsCloneAudioName ? 'var(--accent-color-alpha, rgba(33,150,243,0.05))' : 'transparent'">
  1473. <div *ngIf="!vsCloneAudioName">
  1474. <span class="icon" style="font-size:36px;color:var(--text-secondary);display:block;margin-bottom:6px">audio_file</span>
  1475. <p style="margin:0 0 6px;color:var(--text-primary);font-size:14px;font-weight:500">点击选择音频文件</p>
  1476. <p style="margin:0;color:var(--text-secondary);font-size:12px">支持 MP3、WAV、M4A、OGG、AAC、FLAC 格式,最大 50MB</p>
  1477. <label for="vsCloneAudioInput" class="btn btn-primary" style="margin-top:10px;cursor:pointer;display:inline-flex;font-size:13px">
  1478. <span class="icon" style="font-size:16px">folder_open</span> 选择音频
  1479. </label>
  1480. </div>
  1481. <div *ngIf="vsCloneAudioName">
  1482. <span class="icon" style="font-size:28px;color:var(--accent-color);display:block;margin-bottom:4px">audio_file</span>
  1483. <p style="margin:0 0 2px;color:var(--text-primary);font-size:14px;font-weight:500">{{vsCloneAudioName}}</p>
  1484. <p *ngIf="vsCloneAudioFile" style="margin:0;color:var(--text-secondary);font-size:12px">
  1485. 文件大小: {{((vsCloneAudioFile?.size || 0) / 1024 / 1024).toFixed(2)}} MB
  1486. </p>
  1487. <p *ngIf="vsCloneAudioUrl" style="margin:4px 0 0;color:var(--success-color);font-size:12px">
  1488. <span class="icon" style="font-size:14px;vertical-align:middle">check_circle</span> 已上传
  1489. </p>
  1490. <div style="margin-top:8px;display:flex;gap:8px;justify-content:center" *ngIf="!vsCloneLoading && !vsCloneUploading">
  1491. <label for="vsCloneAudioInput" class="btn" style="cursor:pointer;display:inline-flex;font-size:12px">
  1492. <span class="icon" style="font-size:14px">swap_horiz</span> 更换文件
  1493. </label>
  1494. <button class="btn" (click)="clearVsCloneAudio()" style="font-size:12px">
  1495. <span class="icon" style="font-size:14px">delete</span> 移除
  1496. </button>
  1497. </div>
  1498. <p *ngIf="vsCloneUploading" style="margin:6px 0 0;color:var(--accent-color);font-size:12px">
  1499. <span class="icon" style="font-size:14px;vertical-align:middle">hourglass_empty</span> 正在上传音频...
  1500. </p>
  1501. </div>
  1502. </div>
  1503. </div>
  1504. <div class="form-field">
  1505. <label>音频对应文本(可选)</label>
  1506. <input type="text" [(ngModel)]="vsCloneForm.audioText"
  1507. placeholder="音频中说的文本内容,有助于提升复刻质量"
  1508. [disabled]="vsCloneLoading || vsCloneUploading">
  1509. </div>
  1510. <div style="display:flex;gap:12px">
  1511. <div class="form-field" style="flex:1">
  1512. <label>语言</label>
  1513. <select [(ngModel)]="vsCloneForm.language" [disabled]="vsCloneLoading || vsCloneUploading">
  1514. <option [ngValue]="0">中文</option>
  1515. <option [ngValue]="1">英文</option>
  1516. </select>
  1517. </div>
  1518. <div class="form-field" style="flex:2">
  1519. <label>试听文本(4-80字)*</label>
  1520. <input type="text" [(ngModel)]="vsCloneForm.demoText"
  1521. placeholder="复刻成功后用于生成试听音频的文本"
  1522. [disabled]="vsCloneLoading || vsCloneUploading">
  1523. </div>
  1524. </div>
  1525. <div style="color:var(--error-color);font-size:13px" *ngIf="vsCloneError">
  1526. <span class="icon" style="font-size:14px;vertical-align:middle">error</span> {{vsCloneError}}
  1527. </div>
  1528. <button class="btn btn-primary" (click)="vsStartClone()" [disabled]="vsCloneLoading || vsCloneUploading" style="align-self:flex-start">
  1529. <span class="icon">{{vsCloneLoading || vsCloneUploading ? 'hourglass_empty' : 'content_copy'}}</span>
  1530. {{vsCloneUploading ? '上传音频中...' : vsCloneLoading ? '训练中...' : '开始训练' }}
  1531. </button>
  1532. <!-- 复刻结果 -->
  1533. <div *ngIf="vsCloneResult?.code === 200 && vsCloneResult?.data?.timbre"
  1534. style="margin-top:8px;padding:16px;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)">
  1535. <h4 style="margin:0 0 10px;display:flex;align-items:center;gap:6px">
  1536. <span class="icon" style="color:var(--success-color)">check_circle</span> 复刻成功
  1537. </h4>
  1538. <div style="display:flex;flex-direction:column;gap:6px;font-size:13px">
  1539. <div><strong>声音名称:</strong>{{vsCloneResult.data.timbre.name || vsCloneForm.name}}</div>
  1540. <div *ngIf="vsCloneResult.data.timbre.models?.[0]?.demo_audio">
  1541. <strong>试听音频:</strong>
  1542. <audio controls [src]="vsCloneResult.data.timbre.models[0].demo_audio" preload="metadata" style="height:32px;vertical-align:middle"></audio>
  1543. </div>
  1544. </div>
  1545. <button class="btn btn-primary" style="margin-top:12px"
  1546. (click)="vsSelectClonedTimbre(vsCloneResult.data.timbre.objectId)">
  1547. <span class="icon">arrow_forward</span> 使用此声音生成语音
  1548. </button>
  1549. </div>
  1550. <!-- 已复刻音色快捷列表 -->
  1551. <div *ngIf="vsClonedTimbreList.length > 0" style="margin-top:8px">
  1552. <h4 style="margin:0 0 8px;font-size:14px;color:var(--text-secondary)">我的声音模型</h4>
  1553. <div style="display:flex;flex-wrap:wrap;gap:8px">
  1554. <button class="btn" *ngFor="let t of vsClonedTimbreList"
  1555. (click)="vsSelectClonedTimbre(t.objectId)"
  1556. style="font-size:12px">
  1557. <span class="icon" style="font-size:14px">record_voice_over</span>
  1558. {{t.name}}
  1559. </button>
  1560. </div>
  1561. </div>
  1562. </div>
  1563. <!-- 语音合成表单 -->
  1564. <div *ngIf="vsTtsMode === 'synthesize'" style="display:flex;flex-direction:column;gap:14px">
  1565. <div class="form-field">
  1566. <label>声音模型 *</label>
  1567. <input type="text" [(ngModel)]="vsSynthForm.timbreId"
  1568. placeholder="请选择已训练的声音模型"
  1569. [disabled]="vsSynthLoading">
  1570. </div>
  1571. <!-- 已复刻音色快捷选择 -->
  1572. <div *ngIf="vsClonedTimbreList.length > 0" style="display:flex;flex-wrap:wrap;gap:6px">
  1573. <button class="btn" *ngFor="let t of vsClonedTimbreList"
  1574. [class.btn-primary]="vsSynthForm.timbreId === t.objectId"
  1575. (click)="vsSynthForm.timbreId = t.objectId"
  1576. [disabled]="vsSynthLoading"
  1577. style="font-size:12px">
  1578. {{t.name}}
  1579. </button>
  1580. </div>
  1581. <div class="form-field">
  1582. <label>合成文本 *</label>
  1583. <textarea [(ngModel)]="vsSynthForm.text" rows="4"
  1584. placeholder="请输入要合成的文本内容..."
  1585. [disabled]="vsSynthLoading"
  1586. style="resize:vertical;min-height:80px"></textarea>
  1587. </div>
  1588. <div style="display:flex;gap:12px;flex-wrap:wrap">
  1589. <div class="form-field" style="flex:1;min-width:120px">
  1590. <label>音频格式</label>
  1591. <select [(ngModel)]="vsSynthForm.format" [disabled]="vsSynthLoading">
  1592. <option value="mp3">MP3</option>
  1593. <option value="ogg_opus">OGG Opus</option>
  1594. <option value="pcm">PCM</option>
  1595. </select>
  1596. </div>
  1597. <div class="form-field" style="flex:1;min-width:120px">
  1598. <label>采样率</label>
  1599. <select [(ngModel)]="vsSynthForm.sampleRate" [disabled]="vsSynthLoading">
  1600. <option [ngValue]="8000">8000</option>
  1601. <option [ngValue]="16000">16000</option>
  1602. <option [ngValue]="24000">24000</option>
  1603. <option [ngValue]="48000">48000</option>
  1604. </select>
  1605. </div>
  1606. <div class="form-field" style="flex:1;min-width:120px">
  1607. <label>语速 ({{vsSynthForm.speechRate}})</label>
  1608. <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.speechRate" [disabled]="vsSynthLoading">
  1609. </div>
  1610. <div class="form-field" style="flex:1;min-width:120px">
  1611. <label>音量 ({{vsSynthForm.loudnessRate}})</label>
  1612. <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.loudnessRate" [disabled]="vsSynthLoading">
  1613. </div>
  1614. </div>
  1615. <div style="color:var(--error-color);font-size:13px" *ngIf="vsSynthError">
  1616. <span class="icon" style="font-size:14px;vertical-align:middle">error</span> {{vsSynthError}}
  1617. </div>
  1618. <button class="btn btn-primary" (click)="vsStartSynthesize()" [disabled]="vsSynthLoading" style="align-self:flex-start">
  1619. <span class="icon">{{vsSynthLoading ? 'hourglass_empty' : 'play_arrow'}}</span>
  1620. {{vsSynthLoading ? '合成中...' : '开始语音合成'}}
  1621. </button>
  1622. <!-- 合成结果 -->
  1623. <div *ngIf="vsSynthAudioUrl"
  1624. style="margin-top:8px;padding:16px;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)">
  1625. <h4 style="margin:0 0 10px;display:flex;align-items:center;gap:6px">
  1626. <span class="icon" style="color:var(--success-color)">check_circle</span> 合成成功
  1627. </h4>
  1628. <audio controls [src]="vsSynthAudioUrl" preload="metadata" style="width:100%;margin-bottom:8px"></audio>
  1629. <div style="font-size:13px;display:flex;flex-direction:column;gap:4px">
  1630. <div *ngIf="vsSynthWorkId"><strong>作品ID:</strong><code>{{vsSynthWorkId}}</code></div>
  1631. <div><strong>音频地址:</strong>
  1632. <a [href]="vsSynthAudioUrl" target="_blank" style="color:var(--accent-color);word-break:break-all">{{vsSynthAudioUrl}}</a>
  1633. </div>
  1634. </div>
  1635. </div>
  1636. </div>
  1637. </section>
  1638. <!-- 数字人合成模块 -->
  1639. <section class="dh-page" *ngIf="currentTab === 'digital-human'">
  1640. <!-- 顶部标题栏 -->
  1641. <header class="dh-page-header">
  1642. <div class="dh-header-icon">
  1643. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
  1644. <line x1="12" y1="5" x2="12" y2="19"></line>
  1645. <line x1="5" y1="12" x2="19" y2="12"></line>
  1646. </svg>
  1647. </div>
  1648. <div class="dh-header-text">
  1649. <h1>添加形象</h1>
  1650. <p>上传形象图与音频,一键生成专属数字人口播视频</p>
  1651. </div>
  1652. <button type="button" class="dh-help-btn" (click)="showToast('可以先在「语音合成」中训练声音模型,再用于数字人视频。', 'info', 5000)">
  1653. <span class="dh-help-icon">?</span>
  1654. <span>帮助</span>
  1655. </button>
  1656. </header>
  1657. <!-- ============================================================ -->
  1658. <!-- 4 栏 Step 流程板:参考视频 → 内容与脚本 → 数字人设置 → 视频预览 -->
  1659. <!-- ============================================================ -->
  1660. <div class="dh-board">
  1661. <!-- ===================== Step 1: 参考视频 ===================== -->
  1662. <article class="dh-step-col">
  1663. <header class="dh-step-col__head">
  1664. <span class="dh-step-col__step">Step 1</span>
  1665. <h3 class="dh-step-col__title">参考视频</h3>
  1666. <button type="button" class="dh-chip-btn dh-step-col__action"
  1667. *ngIf="dhRefVideoUrl || dhRefFile || dhRefTranscript"
  1668. (click)="dhRefClear()">清除</button>
  1669. </header>
  1670. <div class="dh-step-col__body">
  1671. <!-- 未选择文件 -->
  1672. <div *ngIf="!dhRefFile && !dhRefVideoUrl">
  1673. <input type="file" id="dhRefVideoInput" accept="video/*" (change)="dhRefSelectFile($event)" hidden>
  1674. <label for="dhRefVideoInput" class="dh-mini-drop dh-ref-drop">
  1675. <span class="dh-mini-drop__title">点击选择参考视频</span>
  1676. <span class="dh-mini-drop__hint">支持常见视频格式,建议 ≤ 100MB;上传后将自动开始分析</span>
  1677. </label>
  1678. <p class="dh-step-col__hint">上传视频后,AI 会自动识别画面与音频,生成口播文本与提示词。</p>
  1679. </div>
  1680. <!-- 已选择文件 / 已分析 -->
  1681. <div *ngIf="dhRefFile || dhRefVideoUrl" class="dh-step-col__stack">
  1682. <div class="dh-ref-preview-card">
  1683. <div class="dh-ref-preview-card__video">
  1684. <video *ngIf="dhRefVideoUrl" controls muted [src]="dhRefVideoUrl"></video>
  1685. </div>
  1686. <div class="dh-ref-preview-card__meta">
  1687. <div class="dh-ref-preview-card__name" [title]="dhRefFileName">{{dhRefFileName || '参考视频'}}</div>
  1688. <div class="dh-ref-preview-card__size" *ngIf="dhRefFileSize > 0">{{(dhRefFileSize / 1024 / 1024).toFixed(2)}} MB</div>
  1689. </div>
  1690. </div>
  1691. <button type="button" class="dh-cta dh-cta--sm"
  1692. *ngIf="dhRefFile && !dhRefUploading && !dhRefAnalyzing && !dhRefVideoId"
  1693. (click)="dhRefUploadAndAnalyze()">开始分析</button>
  1694. <!-- 上传 / 分析进度 -->
  1695. <div class="dh-ref-progress" *ngIf="dhRefUploading">
  1696. <span class="dh-spinner"></span>
  1697. <span>正在上传...{{dhRefUploadProgress | number:'1.0-0'}}%</span>
  1698. </div>
  1699. <div class="dh-ref-progress" *ngIf="dhRefAnalyzing">
  1700. <span class="dh-spinner"></span>
  1701. <span>{{dhRefAnalysisText || '正在分析视频内容...'}}</span>
  1702. </div>
  1703. <!-- AI 分析结果(已自动填入 Step 2 口播文本) -->
  1704. <div class="dh-substep" *ngIf="dhRefTranscript && !dhRefAnalyzing">
  1705. <header class="dh-substep__head">
  1706. <h4>AI 分析结果</h4>
  1707. <span class="dh-chip-btn dh-chip-btn--ghost" style="cursor:default">已同步到口播</span>
  1708. </header>
  1709. <div class="dh-field">
  1710. <label class="dh-field__label"><span>视频文字稿(可编辑)</span></label>
  1711. <textarea class="dh-textarea" rows="4" [(ngModel)]="dhRefTranscript"
  1712. (ngModelChange)="dhRefSyncToTts($event)"></textarea>
  1713. </div>
  1714. <div class="dh-field" *ngIf="dhRefAnalysisText">
  1715. <label class="dh-field__label"><span>内容分析</span></label>
  1716. <div class="dh-ref-analysis">{{dhRefAnalysisText}}</div>
  1717. </div>
  1718. </div>
  1719. <div class="dh-error" *ngIf="dhRefError">{{dhRefError}}</div>
  1720. </div>
  1721. </div>
  1722. </article>
  1723. <!-- ===================== Step 2: 内容与脚本 ===================== -->
  1724. <article class="dh-step-col">
  1725. <header class="dh-step-col__head">
  1726. <span class="dh-step-col__step">Step 2</span>
  1727. <h3 class="dh-step-col__title">内容与脚本</h3>
  1728. <span class="dh-step-col__action dh-step-col__counter"
  1729. *ngIf="dhTtsText">{{(dhTtsText || '').length}} 字</span>
  1730. </header>
  1731. <div class="dh-step-col__body dh-step-col__stack">
  1732. <div class="dh-field">
  1733. <label class="dh-field__label">
  1734. <span>口播文本 <em>*</em></span>
  1735. <span class="dh-field__counter">{{(dhTtsText || '').length}} / 600</span>
  1736. </label>
  1737. <textarea class="dh-textarea" rows="4" maxlength="600" [(ngModel)]="dhTtsText"
  1738. placeholder="输入数字人要朗读的文本,将使用所选声音模型生成语音"></textarea>
  1739. </div>
  1740. <div class="dh-field">
  1741. <label class="dh-field__label">
  1742. <span>补充提示词(可选)</span>
  1743. <span class="dh-field__counter">{{(dhPrompt || '').length}} / 200</span>
  1744. </label>
  1745. <textarea class="dh-textarea" rows="3" maxlength="200" [(ngModel)]="dhPrompt"
  1746. placeholder="可选:强调产品的易用性,缩短文案至 40 字..."></textarea>
  1747. <!-- 预设风格快捷填入 -->
  1748. <div class="dh-preset-row">
  1749. <button type="button" class="dh-preset-chip"
  1750. *ngFor="let p of dhPromptPresets"
  1751. [class.is-active]="dhPrompt === p.value"
  1752. (click)="dhApplyPromptPreset(p.value)"
  1753. [title]="p.value">{{p.label}}</button>
  1754. </div>
  1755. </div>
  1756. <!-- AI 风格助手(折叠在内容栏底部) -->
  1757. <details class="dh-substep dh-chat-fold" [attr.open]="dhChatMessages.length > 0 ? '' : null">
  1758. <summary class="dh-substep__head dh-chat-fold__summary">
  1759. <h4>
  1760. <span class="dh-chat-dot"></span>
  1761. AI 风格助手
  1762. <span class="dh-chat-fold__badge" *ngIf="dhChatMessages.length > 0">{{dhChatMessages.length}}</span>
  1763. </h4>
  1764. <button type="button" class="dh-chip-btn"
  1765. *ngIf="dhChatMessages.length > 0"
  1766. (click)="$event.preventDefault(); $event.stopPropagation(); dhChatClear()">清空</button>
  1767. </summary>
  1768. <p class="dh-chat-fold__desc">用一句话告诉 AI 你想要的语气或风格,它会基于当前口播文本与提示词给出修改建议。</p>
  1769. <div class="dh-chat-quick">
  1770. <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
  1771. (click)="dhChatQuickAsk('请把口播文本改得更口语化、更接地气,像在和朋友聊天')">更口语化</button>
  1772. <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
  1773. (click)="dhChatQuickAsk('请把口播文本改得更专业、像新闻播报的语气')">更专业</button>
  1774. <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
  1775. (click)="dhChatQuickAsk('请把口播文本压缩到 100 字以内,保留核心信息')">压缩到 100 字</button>
  1776. <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
  1777. (click)="dhChatQuickAsk('在保持原意的前提下,开头加一句吸引人的钩子')">加钩子开头</button>
  1778. <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
  1779. (click)="dhChatQuickAsk('请优化补充提示词,强调自信、亲和的镜头表现')">优化提示词</button>
  1780. </div>
  1781. <div class="dh-chat-list" *ngIf="dhChatMessages.length > 0">
  1782. <ng-container *ngFor="let msg of dhChatMessages; let i = index">
  1783. <div class="dh-chat-msg dh-chat-msg--user" *ngIf="msg.role === 'user'">
  1784. <div class="dh-chat-bubble dh-chat-bubble--user">{{msg.content}}</div>
  1785. </div>
  1786. <div class="dh-chat-msg dh-chat-msg--ai" *ngIf="msg.role === 'assistant'">
  1787. <div class="dh-chat-avatar">AI</div>
  1788. <div class="dh-chat-bubble dh-chat-bubble--ai">
  1789. <div class="dh-chat-bubble__text">{{msg.content}}</div>
  1790. <div class="dh-chat-suggest" *ngIf="msg.suggestedTts">
  1791. <div class="dh-chat-suggest__label">建议口播文本</div>
  1792. <div class="dh-chat-suggest__text">{{msg.suggestedTts}}</div>
  1793. </div>
  1794. <div class="dh-chat-suggest" *ngIf="msg.suggestedPrompt">
  1795. <div class="dh-chat-suggest__label">建议补充提示词</div>
  1796. <div class="dh-chat-suggest__text">{{msg.suggestedPrompt}}</div>
  1797. </div>
  1798. <div class="dh-chat-actions" *ngIf="msg.suggestedTts || msg.suggestedPrompt">
  1799. <button type="button" class="dh-chat-apply" *ngIf="msg.suggestedTts"
  1800. [disabled]="msg.appliedTts" (click)="dhChatApply(i, 'tts')">
  1801. {{msg.appliedTts ? '✓ 口播已应用' : '应用到口播'}}
  1802. </button>
  1803. <button type="button" class="dh-chat-apply" *ngIf="msg.suggestedPrompt"
  1804. [disabled]="msg.appliedPrompt" (click)="dhChatApply(i, 'prompt')">
  1805. {{msg.appliedPrompt ? '✓ 提示词已应用' : '应用到提示词'}}
  1806. </button>
  1807. <button type="button" class="dh-chat-apply dh-chat-apply--primary"
  1808. *ngIf="msg.suggestedTts && msg.suggestedPrompt"
  1809. [disabled]="msg.appliedTts && msg.appliedPrompt"
  1810. (click)="dhChatApply(i, 'both')">全部应用</button>
  1811. </div>
  1812. </div>
  1813. </div>
  1814. </ng-container>
  1815. <div class="dh-chat-msg dh-chat-msg--ai" *ngIf="dhChatLoading">
  1816. <div class="dh-chat-avatar">AI</div>
  1817. <div class="dh-chat-bubble dh-chat-bubble--ai">
  1818. <span class="dh-spinner"></span>
  1819. <span style="margin-left:8px">AI 正在思考...</span>
  1820. </div>
  1821. </div>
  1822. </div>
  1823. <div class="dh-chat-inputbar">
  1824. <textarea class="dh-textarea dh-chat-textarea" rows="2"
  1825. [(ngModel)]="dhChatInput" [disabled]="dhChatLoading"
  1826. placeholder="例如:把语气改得更轻松一点、加一个反问句..."
  1827. (keydown.enter)="!$any($event).shiftKey && ($any($event).preventDefault() || dhChatSend())"></textarea>
  1828. <button type="button" class="dh-cta dh-cta--sm dh-chat-send"
  1829. [disabled]="dhChatLoading || !dhChatInput.trim()"
  1830. (click)="dhChatSend()">
  1831. <span *ngIf="!dhChatLoading">发送</span>
  1832. <span *ngIf="dhChatLoading" class="dh-cta__spinner-row">
  1833. <span class="dh-spinner"></span><span>发送中</span>
  1834. </span>
  1835. </button>
  1836. </div>
  1837. <div class="dh-error" *ngIf="dhChatError">{{dhChatError}}</div>
  1838. </details>
  1839. </div>
  1840. </article>
  1841. <!-- ===================== Step 3: 数字人设置 ===================== -->
  1842. <article class="dh-step-col">
  1843. <header class="dh-step-col__head">
  1844. <span class="dh-step-col__step">Step 3</span>
  1845. <h3 class="dh-step-col__title">数字人设置</h3>
  1846. </header>
  1847. <div class="dh-step-col__body dh-step-col__stack">
  1848. <!-- 形象图上传 -->
  1849. <div class="dh-substep">
  1850. <header class="dh-substep__head">
  1851. <h4>形象图上传 <em>*</em></h4>
  1852. </header>
  1853. <input type="file" id="dhImageInput"
  1854. accept="image/jpeg,image/png,image/jpg,image/gif,.jpg,.jpeg,.png,.gif"
  1855. (change)="onDhImageSelected($event)" hidden [disabled]="dhLoading">
  1856. <label for="dhImageInput" class="dh-image-card dh-image-card--empty" *ngIf="!dhImagePreviewUrl">
  1857. <svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
  1858. <path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
  1859. <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
  1860. </svg>
  1861. <span class="dh-image-card__title">点击上传形象图</span>
  1862. <ul class="dh-image-card__hints">
  1863. <li>支持 JPG / PNG / GIF</li>
  1864. <li>建议清晰单人正面照</li>
  1865. <li>最大 20MB</li>
  1866. </ul>
  1867. </label>
  1868. <div class="dh-image-card dh-image-card--filled" *ngIf="dhImagePreviewUrl">
  1869. <img [src]="dhImagePreviewUrl" [alt]="dhImageName || '数字人形象图'">
  1870. <div class="dh-image-card__meta">
  1871. <div class="dh-image-card__name">{{dhImageName || '未命名形象'}}</div>
  1872. <div class="dh-image-card__size" *ngIf="dhImageFile">{{(dhImageFile.size / 1024 / 1024).toFixed(2)}} MB</div>
  1873. <div class="dh-image-card__actions">
  1874. <label for="dhImageInput" class="dh-chip-btn">更换</label>
  1875. <button type="button" class="dh-chip-btn dh-chip-btn--danger" (click)="clearDhImage()">移除</button>
  1876. </div>
  1877. </div>
  1878. </div>
  1879. <div class="dh-field" style="margin-top:10px">
  1880. <label class="dh-field__label">
  1881. <span>形象名称</span>
  1882. <span class="dh-field__counter">{{(dhImageName || '').length}} / 60</span>
  1883. </label>
  1884. <input class="dh-input" type="text" [(ngModel)]="dhImageName" maxlength="60" placeholder="请输入数字人名称">
  1885. </div>
  1886. <!-- 形象准备:raw / 自拍优化 / 对标账号重塑(仅当上传图后显示) -->
  1887. <div *ngIf="dhImageFile" style="margin-top:14px;padding-top:14px;border-top:1px dashed var(--border-color)">
  1888. <app-portrait-prepare
  1889. [selfieFile]="dhImageFile"
  1890. [selfiePreviewUrl]="dhImagePreviewUrl"
  1891. (prepared)="onDhPortraitPrepared($event)">
  1892. </app-portrait-prepare>
  1893. </div>
  1894. </div>
  1895. <!-- 音频来源 -->
  1896. <div class="dh-substep">
  1897. <header class="dh-substep__head">
  1898. <h4>音频来源 <em>*</em></h4>
  1899. </header>
  1900. <div class="dh-segment">
  1901. <button type="button" class="dh-segment__item" [class.is-active]="dhAudioSource === 'tts'" (click)="dhAudioSource = 'tts'; dhError = ''">语音合成</button>
  1902. <button type="button" class="dh-segment__item" [class.is-active]="dhAudioSource === 'upload'" (click)="dhAudioSource = 'upload'; dhError = ''">提供音频</button>
  1903. </div>
  1904. <ng-container *ngIf="dhAudioSource === 'tts'">
  1905. <div class="dh-field" style="margin-top:10px">
  1906. <label class="dh-field__label"><span>选择声音模型 <em>*</em></span></label>
  1907. <button type="button" class="vs-voice-picker-trigger"
  1908. [class.is-empty]="!dhCurrentVoiceSelection"
  1909. [disabled]="dhLoading"
  1910. (click)="openDhVoicePicker()">
  1911. <ng-container *ngIf="dhCurrentVoiceSelection; else dhVoiceEmpty">
  1912. <span class="vs-voice-picker-avatar"
  1913. [class.vs-voice-picker-avatar--mine]="dhCurrentVoiceSelection.source === 'cloned'">
  1914. {{dhCurrentVoiceSelection.source === 'official' ? '知' : '我'}}
  1915. </span>
  1916. <span class="vs-voice-picker-info">
  1917. <span class="vs-voice-picker-name">{{dhCurrentVoiceSelection.name}}</span>
  1918. <span class="vs-voice-picker-src">
  1919. {{dhCurrentVoiceSelection.source === 'official' ? '声音库' : '我的声音'}}
  1920. </span>
  1921. </span>
  1922. </ng-container>
  1923. <ng-template #dhVoiceEmpty>
  1924. <span class="vs-voice-picker-avatar vs-voice-picker-avatar--empty">+</span>
  1925. <span class="vs-voice-picker-info">
  1926. <span class="vs-voice-picker-name">请选择声音模型</span>
  1927. <span class="vs-voice-picker-src">点击选择声音库 / 我的声音</span>
  1928. </span>
  1929. </ng-template>
  1930. <span class="vs-voice-picker-chevron">›</span>
  1931. </button>
  1932. </div>
  1933. <app-voice-picker
  1934. [open]="dhVoicePickerOpen"
  1935. [clonedList]="vsClonedTimbreList"
  1936. [currentSelection]="dhCurrentVoiceSelection"
  1937. (select)="onDhVoicePicked($event)"
  1938. (close)="closeDhVoicePicker()">
  1939. </app-voice-picker>
  1940. </ng-container>
  1941. <ng-container *ngIf="dhAudioSource === 'upload'">
  1942. <div class="dh-field" style="margin-top:10px">
  1943. <label class="dh-field__label"><span>音频文件 <em>*</em></span></label>
  1944. <input type="file" id="dhAudioInput" accept=".mp3,.wav,.m4a,.ogg,.aac,.flac,.wma,audio/*" (change)="onDhAudioSelected($event)" hidden>
  1945. <label for="dhAudioInput" class="dh-mini-drop" *ngIf="!dhUploadAudioName">
  1946. <span class="dh-mini-drop__title">点击选择音频文件</span>
  1947. <span class="dh-mini-drop__hint">支持 MP3 / WAV / M4A / OGG / AAC / FLAC,最大 50MB</span>
  1948. </label>
  1949. <div class="dh-mini-drop dh-mini-drop--filled" *ngIf="dhUploadAudioName">
  1950. <div>
  1951. <div class="dh-mini-drop__name">{{dhUploadAudioName}}</div>
  1952. <div class="dh-mini-drop__sub" *ngIf="dhUploadAudioFile">{{(dhUploadAudioFile.size / 1024 / 1024).toFixed(2)}} MB</div>
  1953. </div>
  1954. <div class="dh-mini-drop__actions">
  1955. <label for="dhAudioInput" class="dh-chip-btn">更换</label>
  1956. <button type="button" class="dh-chip-btn dh-chip-btn--danger" (click)="clearDhAudio()">移除</button>
  1957. </div>
  1958. </div>
  1959. </div>
  1960. </ng-container>
  1961. </div>
  1962. <!-- 输出设置 -->
  1963. <div class="dh-substep">
  1964. <header class="dh-substep__head">
  1965. <h4>输出设置</h4>
  1966. </header>
  1967. <div class="dh-field">
  1968. <label class="dh-field__label"><span>分辨率</span></label>
  1969. <div class="dh-segment">
  1970. <button type="button" class="dh-segment__item" [class.is-active]="dhResolution === '720p'" (click)="dhResolution = '720p'">720P</button>
  1971. <button type="button" class="dh-segment__item" [class.is-active]="dhResolution === '1080p'" (click)="dhResolution = '1080p'">1080P</button>
  1972. </div>
  1973. </div>
  1974. <label class="dh-toggle" style="margin-top:10px">
  1975. <input type="checkbox" [(ngModel)]="dhFastMode">
  1976. <span class="dh-toggle__track"><span class="dh-toggle__thumb"></span></span>
  1977. <span class="dh-toggle__label">快速模式<small>720P 自动启用</small></span>
  1978. </label>
  1979. </div>
  1980. </div>
  1981. </article>
  1982. <!-- ===================== Step 4: 视频预览 ===================== -->
  1983. <article class="dh-step-col dh-step-col--preview">
  1984. <header class="dh-step-col__head">
  1985. <span class="dh-step-col__step">Step 4</span>
  1986. <h3 class="dh-step-col__title">视频预览</h3>
  1987. </header>
  1988. <div class="dh-step-col__body dh-step-col__stack">
  1989. <!-- 预览主区 -->
  1990. <div class="dh-preview-stage">
  1991. <!-- 空状态 -->
  1992. <div class="dh-preview-stage__empty" *ngIf="dhStep === 'form' && !dhResultVideoUrl">
  1993. <div class="dh-preview-stage__icon">
  1994. <svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
  1995. <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
  1996. </svg>
  1997. </div>
  1998. <p class="dh-preview-stage__title">完成以上步骤后</p>
  1999. <p class="dh-preview-stage__sub">点击下方按钮即可生成数字人视频</p>
  2000. </div>
  2001. <!-- 生成中 -->
  2002. <div class="dh-preview-stage__loading" *ngIf="dhStep === 'generating'">
  2003. <span class="dh-spinner dh-spinner--lg"></span>
  2004. <p class="dh-preview-stage__title">{{dhStatusText || '正在生成...'}}</p>
  2005. <div class="dh-progress-bar"><div class="dh-progress-bar__fill" [style.width]="dhProgress + '%'"></div></div>
  2006. <p class="dh-preview-stage__sub">{{dhProgress | number:'1.0-0'}}%</p>
  2007. </div>
  2008. <!-- 完成 -->
  2009. <div class="dh-preview-stage__done" *ngIf="dhStep === 'done' && dhResultVideoUrl">
  2010. <video controls [src]="dhResultVideoUrl"></video>
  2011. </div>
  2012. <!-- 失败 -->
  2013. <div class="dh-preview-stage__error" *ngIf="dhStep === 'done' && !dhResultVideoUrl && dhError">
  2014. <span class="dh-preview-stage__cross">!</span>
  2015. <p class="dh-preview-stage__title">生成失败</p>
  2016. <p class="dh-preview-stage__sub">{{dhError}}</p>
  2017. </div>
  2018. </div>
  2019. <!-- 主 CTA -->
  2020. <button type="button" class="dh-cta dh-cta--xl"
  2021. *ngIf="dhStep !== 'done' || !dhResultVideoUrl"
  2022. (click)="dhStartGeneration()"
  2023. [disabled]="dhLoading || dhStep === 'generating'">
  2024. <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2025. <polygon points="5 3 19 12 5 21 5 3"></polygon>
  2026. </svg>
  2027. <span>{{dhStep === 'generating' ? '生成中...' : '立即生成'}}</span>
  2028. </button>
  2029. <div class="dh-error" *ngIf="dhError && dhStep !== 'done'">{{dhError}}</div>
  2030. <!-- 步骤状态清单 -->
  2031. <ul class="dh-checklist">
  2032. <li class="dh-checklist__item" [class.is-done]="!!dhRefTranscript">
  2033. <span class="dh-checklist__dot"></span>
  2034. <span class="dh-checklist__label">分析参考视频</span>
  2035. <span class="dh-checklist__status">{{dhRefTranscript ? '完成' : (dhRefAnalyzing ? '进行中' : '待处理')}}</span>
  2036. </li>
  2037. <li class="dh-checklist__item" [class.is-done]="!!(dhTtsText && dhTtsText.trim())">
  2038. <span class="dh-checklist__dot"></span>
  2039. <span class="dh-checklist__label">提取口播内容</span>
  2040. <span class="dh-checklist__status">{{dhTtsText && dhTtsText.trim() ? '完成' : '待处理'}}</span>
  2041. </li>
  2042. <li class="dh-checklist__item"
  2043. [class.is-done]="dhProgress >= 25 && dhStep !== 'form'"
  2044. [class.is-active]="dhSynthesizing || (dhAudioSource === 'upload' && dhUploadingAudio)">
  2045. <span class="dh-checklist__dot"></span>
  2046. <span class="dh-checklist__label">音频合成中</span>
  2047. <span class="dh-checklist__status">{{(dhSynthesizing || dhUploadingAudio) ? '进行中' : (dhProgress >= 25 ? '完成' : '待处理')}}</span>
  2048. </li>
  2049. <li class="dh-checklist__item"
  2050. [class.is-done]="dhStep === 'done' && !!dhResultVideoUrl"
  2051. [class.is-active]="dhStep === 'generating' && dhProgress >= 50">
  2052. <span class="dh-checklist__dot"></span>
  2053. <span class="dh-checklist__label">渲染数字人</span>
  2054. <span class="dh-checklist__status">{{dhStep === 'done' && dhResultVideoUrl ? '完成' : (dhStep === 'generating' && dhProgress >= 50 ? '进行中' : '待处理')}}</span>
  2055. </li>
  2056. <li class="dh-checklist__item" [class.is-done]="dhStep === 'done' && !!dhResultVideoUrl">
  2057. <span class="dh-checklist__dot"></span>
  2058. <span class="dh-checklist__label">视频生成导出</span>
  2059. <span class="dh-checklist__status">{{dhStep === 'done' && dhResultVideoUrl ? '完成' : '待处理'}}</span>
  2060. </li>
  2061. </ul>
  2062. <!-- 完成后的操作栏 -->
  2063. <div class="dh-preview-actions" *ngIf="dhStep === 'done' && dhResultVideoUrl">
  2064. <a [href]="dhResultVideoUrl" download class="dh-preview-action">
  2065. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2066. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  2067. <polyline points="7 10 12 15 17 10"></polyline>
  2068. <line x1="12" y1="15" x2="12" y2="3"></line>
  2069. </svg>
  2070. <span>下载视频</span>
  2071. </a>
  2072. <a [href]="dhResultVideoUrl" target="_blank" class="dh-preview-action">
  2073. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2074. <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
  2075. <polyline points="15 3 21 3 21 9"></polyline>
  2076. <line x1="10" y1="14" x2="21" y2="3"></line>
  2077. </svg>
  2078. <span>新窗口播放</span>
  2079. </a>
  2080. <button type="button" class="dh-preview-action" (click)="dhReset()">
  2081. <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2082. <polyline points="23 4 23 10 17 10"></polyline>
  2083. <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
  2084. </svg>
  2085. <span>重新生成</span>
  2086. </button>
  2087. </div>
  2088. </div>
  2089. </article>
  2090. </div>
  2091. <!-- 我的形象库 -->
  2092. <section class="dh-gallery">
  2093. <header class="dh-gallery__header">
  2094. <div>
  2095. <h2>我的形象库</h2>
  2096. <p>查看您创建的所有数字人作品</p>
  2097. </div>
  2098. <button type="button" class="dh-refresh-btn" (click)="refreshDigitalHumanGallery()">
  2099. <span class="dh-refresh-icon">↻</span>
  2100. <span>刷新</span>
  2101. </button>
  2102. </header>
  2103. <div class="dh-gallery__empty" *ngIf="!getDigitalHumanResults().length">
  2104. <div class="dh-gallery__empty-icon">🎬</div>
  2105. <p>还没有数字人作品,先在上方创建一个吧</p>
  2106. </div>
  2107. <div class="dh-gallery__grid" *ngIf="getDigitalHumanResults().length">
  2108. <article class="dh-gallery__card" *ngFor="let r of getDigitalHumanResults()">
  2109. <div class="dh-gallery__media">
  2110. <video [src]="r.url" preload="metadata" muted playsinline></video>
  2111. <a [href]="r.url" target="_blank" class="dh-gallery__play" title="播放">▶</a>
  2112. </div>
  2113. <div class="dh-gallery__body">
  2114. <div class="dh-gallery__title-row">
  2115. <h4 class="dh-gallery__title" [title]="r.title">{{r.title}}</h4>
  2116. <div class="dh-gallery__icons">
  2117. <a [href]="r.url" download class="dh-gallery__icon-btn" title="下载">⬇</a>
  2118. <button type="button" class="dh-gallery__icon-btn dh-gallery__icon-btn--danger" (click)="removeResult(r.id)" title="删除">🗑</button>
  2119. </div>
  2120. </div>
  2121. <div class="dh-gallery__info">
  2122. <span>{{r.quality || '—'}}</span>
  2123. <span>{{r.created_at | date:'yyyy/M/d HH:mm:ss'}}</span>
  2124. <span class="dh-gallery__chip">MP4</span>
  2125. </div>
  2126. </div>
  2127. </article>
  2128. </div>
  2129. </section>
  2130. </section>
  2131. <!-- 视频生成工作流模块 -->
  2132. <section class="dh-page vg-page" *ngIf="false && currentTab === 'video-generation'">
  2133. <header class="dh-page-header">
  2134. <div class="dh-header-icon">
  2135. <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  2136. <polygon points="23 7 16 12 23 17 23 7"></polygon>
  2137. <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
  2138. </svg>
  2139. </div>
  2140. <div class="dh-header-text">
  2141. <h1>视频生成工作流</h1>
  2142. <p>上传视频 → 拆解分析 → 生成脚本 → AI 配图 → 语音配音 → 视频合成</p>
  2143. </div>
  2144. </header>
  2145. <!-- 步骤进度条 -->
  2146. <div class="vg-step-bar">
  2147. <ng-container *ngFor="let label of vgStepLabels; let i = index">
  2148. <div class="vg-step-item"
  2149. [class.is-active]="vgStep === i + 1"
  2150. [class.is-done]="vgStep > i + 1">
  2151. <div class="vg-step-badge">{{vgStep > i + 1 ? '✓' : i + 1}}</div>
  2152. <span class="vg-step-label">{{label}}</span>
  2153. </div>
  2154. <div *ngIf="i < vgStepLabels.length - 1"
  2155. class="vg-step-connector"
  2156. [class.is-done]="vgStep > i + 1"></div>
  2157. </ng-container>
  2158. </div>
  2159. <!-- 错误提示 -->
  2160. <div *ngIf="vgError" style="margin-bottom:16px;padding:12px 16px;background:rgba(244,67,54,0.08);border-radius:8px;color:var(--error-color);font-size:13px;display:flex;align-items:center;gap:8px">
  2161. <span class="icon" style="font-size:16px">error</span>
  2162. {{vgError}}
  2163. <button class="btn btn-sm" style="margin-left:auto" (click)="vgError = ''">
  2164. <span class="icon" style="font-size:14px">close</span>
  2165. </button>
  2166. </div>
  2167. <!-- ===== Step 1: 上传视频 ===== -->
  2168. <section class="vg-step-card" [attr.data-state]="vgGetStepState(1)" [class.is-collapsed]="!vgIsStepExpanded(1)">
  2169. <header class="vg-step-card__header" (click)="vgToggleStepExpand(1)">
  2170. <div class="vg-step-card__num">{{vgStep > 1 ? '✓' : '1'}}</div>
  2171. <div class="vg-step-card__meta">
  2172. <h3>{{vgStepLabels[0]}}</h3>
  2173. <p>{{vgStepSubtitles[0]}}</p>
  2174. </div>
  2175. <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(1)">{{ vgGetStepState(1) === 'done' ? '已完成' : vgGetStepState(1) === 'active' ? '进行中' : '待解锁' }}</div>
  2176. <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(1) === 'done'" (click)="$event.stopPropagation(); vgEditStep(1)" title="返回此步骤编辑">编辑</button>
  2177. <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(1) !== 'done'" tabindex="-1" aria-label="展开/折叠">
  2178. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  2179. </button>
  2180. </header>
  2181. <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(1)">
  2182. <div style="border:2px dashed var(--border-color);border-radius:12px;padding:40px;text-align:center;background:var(--bg-secondary);position:relative"
  2183. [style.borderColor]="vgSourceFile ? 'var(--accent-color)' : 'var(--border-color)'">
  2184. <input type="file" accept="video/*" (change)="vgSelectFile($event)"
  2185. style="position:absolute;inset:0;opacity:0;cursor:pointer">
  2186. <div *ngIf="!vgSourceFile">
  2187. <svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
  2188. style="display:block;margin:0 auto 12px;color:var(--text-secondary)">
  2189. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  2190. <polyline points="17 8 12 3 7 8"></polyline>
  2191. <line x1="12" y1="3" x2="12" y2="15"></line>
  2192. </svg>
  2193. <p style="margin:0;font-size:15px;color:var(--text-primary)">点击或拖拽上传视频文件</p>
  2194. <p style="margin:6px 0 0;font-size:12px;color:var(--text-secondary)">支持 MP4、MOV、AVI、WebM 等格式</p>
  2195. </div>
  2196. <div *ngIf="vgSourceFile" style="display:flex;align-items:center;gap:16px;justify-content:center">
  2197. <span class="icon" style="font-size:36px;color:var(--accent-color)">movie</span>
  2198. <div style="text-align:left">
  2199. <p style="margin:0;font-size:14px;font-weight:500">{{vgSourceFileName}}</p>
  2200. <p style="margin:4px 0 0;font-size:12px;color:var(--text-secondary)">
  2201. {{(vgSourceFile.size / 1024 / 1024).toFixed(1)}} MB
  2202. </p>
  2203. </div>
  2204. </div>
  2205. </div>
  2206. <!-- 视频预览 -->
  2207. <div *ngIf="vgSourceVideoUrl" style="margin-top:16px;text-align:center">
  2208. <video [src]="vgSourceVideoUrl" controls
  2209. style="max-width:100%;max-height:300px;border-radius:8px;background:#000"></video>
  2210. </div>
  2211. <!-- 上传进度 -->
  2212. <div *ngIf="vgUploading" style="margin-top:16px">
  2213. <div style="display:flex;justify-content:space-between;margin-bottom:6px">
  2214. <span style="font-size:13px">正在上传...</span>
  2215. <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{vgUploadProgress}}%</span>
  2216. </div>
  2217. <div class="progress-bar" style="height:6px">
  2218. <div class="progress-fill" [style.width]="vgUploadProgress + '%'"></div>
  2219. </div>
  2220. </div>
  2221. <div style="margin-top:20px;display:flex;justify-content:flex-end">
  2222. <button class="btn btn-primary" (click)="vgUploadAndAnalyze()"
  2223. [disabled]="!vgSourceFile || vgUploading">
  2224. <span class="icon">{{vgUploading ? 'hourglass_empty' : 'upload'}}</span>
  2225. {{vgUploading ? '上传中...' : '上传并开始分析'}}
  2226. </button>
  2227. </div>
  2228. </div>
  2229. </section>
  2230. <!-- ===== Step 2: 视频解析 ===== -->
  2231. <section class="vg-step-card" [attr.data-state]="vgGetStepState(2)" [class.is-collapsed]="!vgIsStepExpanded(2)">
  2232. <header class="vg-step-card__header" (click)="vgToggleStepExpand(2)">
  2233. <div class="vg-step-card__num">{{vgStep > 2 ? '✓' : '2'}}</div>
  2234. <div class="vg-step-card__meta">
  2235. <h3>{{vgStepLabels[1]}}</h3>
  2236. <p>{{vgStepSubtitles[1]}}</p>
  2237. </div>
  2238. <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(2)">{{ vgGetStepState(2) === 'done' ? '已完成' : vgGetStepState(2) === 'active' ? '进行中' : '待解锁' }}</div>
  2239. <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(2) === 'done'" (click)="$event.stopPropagation(); vgEditStep(2)" title="返回此步骤编辑">编辑</button>
  2240. <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(2) !== 'done'" tabindex="-1" aria-label="展开/折叠">
  2241. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  2242. </button>
  2243. </header>
  2244. <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(2)">
  2245. <!-- 解析状态 -->
  2246. <div *ngIf="vgAnalyzing" style="padding:24px;text-align:center;background:var(--bg-secondary);border-radius:10px;margin-bottom:16px">
  2247. <div class="spinner" style="margin:0 auto 12px"></div>
  2248. <p style="margin:0;color:var(--text-primary)">{{vgAnalysisText}}</p>
  2249. </div>
  2250. <!-- AI分析结果 -->
  2251. <div *ngIf="!vgAnalyzing && vgAnalysisText" style="padding:16px;background:var(--bg-secondary);border-radius:10px;margin-bottom:16px;border:1px solid var(--border-color)">
  2252. <h4 style="margin:0 0 10px;display:flex;align-items:center;gap:6px">
  2253. <span class="icon" style="color:var(--accent-color)">smart_toy</span> AI 内容分析
  2254. </h4>
  2255. <pre style="margin:0;white-space:pre-wrap;font-size:13px;line-height:1.6;color:var(--text-primary);font-family:inherit">{{vgAnalysisText}}</pre>
  2256. </div>
  2257. <!-- 转录文字稿 -->
  2258. <div style="margin-bottom:16px">
  2259. <label style="display:block;font-size:13px;font-weight:500;margin-bottom:6px">
  2260. <span class="icon" style="font-size:14px;vertical-align:middle">description</span> 转录文字稿
  2261. </label>
  2262. <textarea [(ngModel)]="vgTranscript" rows="8"
  2263. placeholder="视频转录文字稿将在此显示,也可手动输入或修改..."
  2264. style="width:100%;resize:vertical;min-height:120px;border:1px solid var(--border-color);border-radius:8px;padding:12px;font-size:13px;line-height:1.6;background:var(--bg-primary);color:var(--text-primary)"></textarea>
  2265. </div>
  2266. <div style="display:flex;justify-content:space-between">
  2267. <button class="btn" (click)="vgPrevStep()">
  2268. <span class="icon">arrow_back</span> 上一步
  2269. </button>
  2270. <button class="btn btn-primary" (click)="vgConfirmAnalysis()"
  2271. [disabled]="!vgTranscript.trim() || vgAnalyzing">
  2272. <span class="icon">arrow_forward</span> 确认并生成脚本
  2273. </button>
  2274. </div>
  2275. </div>
  2276. </section>
  2277. <!-- ===== Step 3: 脚本生成 ===== -->
  2278. <section class="vg-step-card" [attr.data-state]="vgGetStepState(3)" [class.is-collapsed]="!vgIsStepExpanded(3)">
  2279. <header class="vg-step-card__header" (click)="vgToggleStepExpand(3)">
  2280. <div class="vg-step-card__num">{{vgStep > 3 ? '✓' : '3'}}</div>
  2281. <div class="vg-step-card__meta">
  2282. <h3>{{vgStepLabels[2]}}</h3>
  2283. <p>{{vgStepSubtitles[2]}}</p>
  2284. </div>
  2285. <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(3)">{{ vgGetStepState(3) === 'done' ? '已完成' : vgGetStepState(3) === 'active' ? '进行中' : '待解锁' }}</div>
  2286. <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(3) === 'done'" (click)="$event.stopPropagation(); vgEditStep(3)" title="返回此步骤编辑">编辑</button>
  2287. <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(3) !== 'done'" tabindex="-1" aria-label="展开/折叠">
  2288. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  2289. </button>
  2290. </header>
  2291. <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(3)">
  2292. <div *ngIf="vgGeneratingScript" style="padding:24px;text-align:center;background:var(--bg-secondary);border-radius:10px;margin-bottom:16px">
  2293. <div class="spinner" style="margin:0 auto 12px"></div>
  2294. <p style="margin:0;color:var(--text-primary)">AI 正在生成分镜脚本...</p>
  2295. </div>
  2296. <!-- 脚本片段列表 -->
  2297. <div *ngIf="!vgGeneratingScript && vgScriptSegments.length > 0" style="display:flex;flex-direction:column;gap:12px;margin-bottom:16px">
  2298. <div *ngFor="let seg of vgScriptSegments; let i = index"
  2299. style="padding:14px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2300. <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
  2301. <span style="background:var(--accent-color);color:#fff;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600">
  2302. {{seg.id}}
  2303. </span>
  2304. </div>
  2305. <div style="margin-bottom:8px">
  2306. <label style="font-size:11px;color:var(--text-secondary);display:block;margin-bottom:4px">旁白文本</label>
  2307. <textarea [(ngModel)]="seg.narration" rows="2"
  2308. style="width:100%;resize:vertical;border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:13px;background:var(--bg-primary);color:var(--text-primary)"></textarea>
  2309. </div>
  2310. <div>
  2311. <label style="font-size:11px;color:var(--text-secondary);display:block;margin-bottom:4px">配图提示词(中文,用于 AI 绘画)</label>
  2312. <textarea [(ngModel)]="seg.imagePrompt" rows="2"
  2313. style="width:100%;resize:vertical;border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:12px;font-family:monospace;background:var(--bg-primary);color:var(--text-primary)"></textarea>
  2314. </div>
  2315. <div *ngIf="seg.imagePromptCn" style="margin-top:6px">
  2316. <label style="font-size:11px;color:var(--text-secondary);display:block;margin-bottom:4px">配图中文说明</label>
  2317. <div style="padding:8px;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;font-size:12px;color:var(--text-secondary)">{{seg.imagePromptCn}}</div>
  2318. </div>
  2319. </div>
  2320. </div>
  2321. <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
  2322. <button class="btn" (click)="vgPrevStep()">
  2323. <span class="icon">arrow_back</span> 上一步
  2324. </button>
  2325. <div style="display:flex;gap:8px">
  2326. <button class="btn" (click)="vgRegenerateScript()" [disabled]="vgGeneratingScript">
  2327. <span class="icon">refresh</span> 重新生成
  2328. </button>
  2329. <button class="btn btn-primary" (click)="vgConfirmScript()"
  2330. [disabled]="vgScriptSegments.length === 0 || vgGeneratingScript">
  2331. <span class="icon">arrow_forward</span> 确认并配图
  2332. </button>
  2333. </div>
  2334. </div>
  2335. </div>
  2336. </section>
  2337. <!-- ===== Step 4: 配图生成 ===== -->
  2338. <section class="vg-step-card" [attr.data-state]="vgGetStepState(4)" [class.is-collapsed]="!vgIsStepExpanded(4)">
  2339. <header class="vg-step-card__header" (click)="vgToggleStepExpand(4)">
  2340. <div class="vg-step-card__num">{{vgStep > 4 ? '✓' : '4'}}</div>
  2341. <div class="vg-step-card__meta">
  2342. <h3>{{vgStepLabels[3]}}</h3>
  2343. <p>{{vgStepSubtitles[3]}}</p>
  2344. </div>
  2345. <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(4)">{{ vgGetStepState(4) === 'done' ? '已完成' : vgGetStepState(4) === 'active' ? '进行中' : '待解锁' }}</div>
  2346. <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(4) === 'done'" (click)="$event.stopPropagation(); vgEditStep(4)" title="返回此步骤编辑">编辑</button>
  2347. <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(4) !== 'done'" tabindex="-1" aria-label="展开/折叠">
  2348. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  2349. </button>
  2350. </header>
  2351. <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(4)">
  2352. <!-- 配图进度 -->
  2353. <div *ngIf="vgGeneratingImages" style="margin-bottom:16px">
  2354. <div style="display:flex;justify-content:space-between;margin-bottom:6px">
  2355. <span style="font-size:13px">正在生成配图...</span>
  2356. <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{vgImageProgress}}%</span>
  2357. </div>
  2358. <div class="progress-bar" style="height:6px">
  2359. <div class="progress-fill" [style.width]="vgImageProgress + '%'"></div>
  2360. </div>
  2361. </div>
  2362. <!-- 图片网格 -->
  2363. <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;margin-bottom:16px">
  2364. <div *ngFor="let img of vgImageResults"
  2365. style="background:var(--bg-secondary);border-radius:10px;overflow:hidden;border:1px solid var(--border-color)">
  2366. <div style="aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;background:var(--bg-tertiary)">
  2367. <img *ngIf="img.imageUrl" [src]="img.imageUrl" style="width:100%;height:100%;object-fit:cover">
  2368. <div *ngIf="!img.imageUrl && img.status === 'generating'" class="spinner"></div>
  2369. <span *ngIf="!img.imageUrl && img.status === 'pending'" class="icon" style="font-size:32px;color:var(--text-secondary)">image</span>
  2370. <span *ngIf="!img.imageUrl && img.status === 'failed'" class="icon" style="font-size:32px;color:var(--error-color)">broken_image</span>
  2371. </div>
  2372. <div style="padding:8px">
  2373. <span style="font-size:12px;font-weight:600">{{img.id}}</span>
  2374. <span style="font-size:11px;margin-left:6px;padding:2px 6px;border-radius:4px"
  2375. [style.background]="img.status === 'done' ? 'rgba(76,175,80,0.15)' : img.status === 'failed' ? 'rgba(244,67,54,0.15)' : img.status === 'generating' ? 'rgba(33,150,243,0.15)' : 'var(--bg-tertiary)'"
  2376. [style.color]="img.status === 'done' ? 'var(--success-color)' : img.status === 'failed' ? 'var(--error-color)' : 'var(--text-secondary)'">
  2377. {{img.status === 'done' ? '完成' : img.status === 'failed' ? '失败' : img.status === 'generating' ? '生成中' : '待生成'}}
  2378. </span>
  2379. </div>
  2380. </div>
  2381. </div>
  2382. <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
  2383. <button class="btn" (click)="vgPrevStep()">
  2384. <span class="icon">arrow_back</span> 上一步
  2385. </button>
  2386. <div style="display:flex;gap:8px">
  2387. <button class="btn btn-primary" (click)="vgGenerateImages()"
  2388. [disabled]="vgGeneratingImages"
  2389. *ngIf="vgImageResults.some(r => r.status === 'pending')">
  2390. <span class="icon">auto_fix_high</span>
  2391. {{vgGeneratingImages ? '生成中...' : '开始生成配图'}}
  2392. </button>
  2393. <button class="btn btn-primary" (click)="vgConfirmImages()"
  2394. [disabled]="vgGeneratingImages">
  2395. <span class="icon">arrow_forward</span> 下一步:配音
  2396. </button>
  2397. </div>
  2398. </div>
  2399. </div>
  2400. </section>
  2401. <!-- ===== Step 5: 语音配音 ===== -->
  2402. <section class="vg-step-card" [attr.data-state]="vgGetStepState(5)" [class.is-collapsed]="!vgIsStepExpanded(5)">
  2403. <header class="vg-step-card__header" (click)="vgToggleStepExpand(5)">
  2404. <div class="vg-step-card__num">{{vgStep > 5 ? '✓' : '5'}}</div>
  2405. <div class="vg-step-card__meta">
  2406. <h3>{{vgStepLabels[4]}}</h3>
  2407. <p>{{vgStepSubtitles[4]}}</p>
  2408. </div>
  2409. <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(5)">{{ vgGetStepState(5) === 'done' ? '已完成' : vgGetStepState(5) === 'active' ? '进行中' : '待解锁' }}</div>
  2410. <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(5) === 'done'" (click)="$event.stopPropagation(); vgEditStep(5)" title="返回此步骤编辑">编辑</button>
  2411. <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(5) !== 'done'" tabindex="-1" aria-label="展开/折叠">
  2412. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  2413. </button>
  2414. </header>
  2415. <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(5)">
  2416. <!-- 声音模型选择 -->
  2417. <div style="margin-bottom:20px;padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2418. <div class="form-field">
  2419. <label>选择声音模型</label>
  2420. <button type="button" class="vs-voice-picker-trigger"
  2421. [class.is-empty]="!vgCurrentVoiceSelection"
  2422. [disabled]="vgSynthesizing"
  2423. (click)="openVgVoicePicker()">
  2424. <ng-container *ngIf="vgCurrentVoiceSelection; else vgVoiceEmpty">
  2425. <span class="vs-voice-picker-avatar"
  2426. [class.vs-voice-picker-avatar--mine]="vgCurrentVoiceSelection.source === 'cloned'">
  2427. {{vgCurrentVoiceSelection.source === 'official' ? '知' : '我'}}
  2428. </span>
  2429. <span class="vs-voice-picker-info">
  2430. <span class="vs-voice-picker-name">{{vgCurrentVoiceSelection.name}}</span>
  2431. <span class="vs-voice-picker-src">
  2432. {{vgCurrentVoiceSelection.source === 'official' ? '声音库' : '我的声音'}}
  2433. </span>
  2434. </span>
  2435. </ng-container>
  2436. <ng-template #vgVoiceEmpty>
  2437. <span class="vs-voice-picker-avatar vs-voice-picker-avatar--empty">+</span>
  2438. <span class="vs-voice-picker-info">
  2439. <span class="vs-voice-picker-name">请选择声音模型</span>
  2440. <span class="vs-voice-picker-src">点击选择声音库 / 我的声音</span>
  2441. </span>
  2442. </ng-template>
  2443. <span class="vs-voice-picker-chevron">›</span>
  2444. </button>
  2445. <app-voice-picker
  2446. [open]="vgVoicePickerOpen"
  2447. [clonedList]="vsClonedTimbreList"
  2448. [currentSelection]="vgCurrentVoiceSelection"
  2449. (select)="onVgVoicePicked($event)"
  2450. (close)="closeVgVoicePicker()">
  2451. </app-voice-picker>
  2452. </div>
  2453. </div>
  2454. <!-- 旁白列表预览 -->
  2455. <div style="margin-bottom:16px">
  2456. <h4 style="margin:0 0 10px;font-size:14px">待配音片段 ({{vgScriptSegments.length}})</h4>
  2457. <div style="display:flex;flex-direction:column;gap:8px;max-height:300px;overflow-y:auto">
  2458. <div *ngFor="let seg of vgScriptSegments; let i = index"
  2459. style="display:flex;align-items:center;gap:10px;padding:10px;background:var(--bg-secondary);border-radius:8px;font-size:13px">
  2460. <span style="background:var(--accent-color);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;flex-shrink:0">{{seg.id}}</span>
  2461. <span style="flex:1;color:var(--text-primary)">{{seg.narration}}</span>
  2462. <span *ngIf="vgAudioUrls.length > i" class="icon" style="color:var(--success-color);flex-shrink:0">check_circle</span>
  2463. </div>
  2464. </div>
  2465. </div>
  2466. <!-- 配音进度 -->
  2467. <div *ngIf="vgSynthesizing" style="margin-bottom:16px;padding:16px;background:var(--bg-secondary);border-radius:10px;text-align:center">
  2468. <div class="spinner" style="margin:0 auto 8px"></div>
  2469. <p style="margin:0;font-size:13px">正在逐段合成语音... ({{vgAudioUrls.length}}/{{vgScriptSegments.length}})</p>
  2470. </div>
  2471. <!-- 已合成音频试听 -->
  2472. <div *ngIf="vgAudioUrls.length > 0 && !vgSynthesizing" style="margin-bottom:16px;padding:12px;background:var(--bg-secondary);border-radius:10px">
  2473. <h4 style="margin:0 0 8px;font-size:13px">已合成 {{vgAudioUrls.length}} 段音频</h4>
  2474. <div style="display:flex;flex-wrap:wrap;gap:8px">
  2475. <div *ngFor="let au of vgAudioUrls" style="display:flex;align-items:center;gap:6px">
  2476. <span style="font-size:11px;font-weight:600">{{au.segId}}</span>
  2477. <audio [src]="au.audioUrl" controls style="height:28px"></audio>
  2478. </div>
  2479. </div>
  2480. </div>
  2481. <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
  2482. <button class="btn" (click)="vgPrevStep()">
  2483. <span class="icon">arrow_back</span> 上一步
  2484. </button>
  2485. <div style="display:flex;gap:8px">
  2486. <button class="btn btn-primary" (click)="vgSynthesizeAll()"
  2487. [disabled]="vgSynthesizing || !vgCurrentVoiceSelection">
  2488. <span class="icon">{{vgSynthesizing ? 'hourglass_empty' : 'mic'}}</span>
  2489. {{vgSynthesizing ? '合成中...' : (vgAudioUrls.length > 0 ? '重新合成' : '开始配音')}}
  2490. </button>
  2491. <button class="btn btn-primary" (click)="vgConfirmVoice()" [disabled]="vgSynthesizing">
  2492. <span class="icon">arrow_forward</span> 下一步:视频合成
  2493. </button>
  2494. </div>
  2495. </div>
  2496. </div>
  2497. </section>
  2498. <!-- ===== Step 6: 视频合成 / 结果汇总 ===== -->
  2499. <section class="vg-step-card" [attr.data-state]="vgGetStepState(6)" [class.is-collapsed]="!vgIsStepExpanded(6)">
  2500. <header class="vg-step-card__header" (click)="vgToggleStepExpand(6)">
  2501. <div class="vg-step-card__num">{{vgStep > 6 ? '✓' : '6'}}</div>
  2502. <div class="vg-step-card__meta">
  2503. <h3>{{vgStepLabels[5]}}</h3>
  2504. <p>{{vgStepSubtitles[5]}}</p>
  2505. </div>
  2506. <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(6)">{{ vgGetStepState(6) === 'done' ? '已完成' : vgGetStepState(6) === 'active' ? '进行中' : '待解锁' }}</div>
  2507. <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(6) !== 'done'" tabindex="-1" aria-label="展开/折叠">
  2508. <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
  2509. </button>
  2510. </header>
  2511. <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(6)">
  2512. <!-- 合成进度 -->
  2513. <div *ngIf="vgCompositing" style="margin-bottom:20px">
  2514. <div style="display:flex;justify-content:space-between;margin-bottom:6px">
  2515. <span style="font-size:13px">正在合成视频(下载素材 → 逐段合成 → 拼接)...</span>
  2516. <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{vgCompositingProgress}}%</span>
  2517. </div>
  2518. <div class="progress-bar" style="height:6px">
  2519. <div class="progress-fill" [style.width]="vgCompositingProgress + '%'"></div>
  2520. </div>
  2521. </div>
  2522. <!-- 合成完成:视频播放器 -->
  2523. <div *ngIf="vgFinalVideoUrl" style="margin-bottom:20px;padding:20px;background:var(--bg-secondary);border-radius:12px;border:2px solid var(--success-color)">
  2524. <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
  2525. <span class="icon" style="color:var(--success-color);font-size:24px">check_circle</span>
  2526. <h4 style="margin:0;font-size:16px">视频合成完成</h4>
  2527. </div>
  2528. <video controls [src]="vgFinalVideoUrl" style="width:100%;max-height:500px;border-radius:8px;background:#000" preload="metadata"></video>
  2529. <div style="display:flex;gap:8px;margin-top:12px">
  2530. <a [href]="vgFinalVideoUrl" download class="btn btn-primary" style="text-decoration:none">
  2531. <span class="icon">download</span> 下载视频
  2532. </a>
  2533. </div>
  2534. </div>
  2535. <!-- 合成错误提示 -->
  2536. <div *ngIf="vgError && !vgCompositing" style="margin-bottom:16px;padding:12px 16px;background:rgba(244,67,54,0.1);border:1px solid rgba(244,67,54,0.3);border-radius:8px;color:var(--error-color);font-size:13px">
  2537. <span class="icon" style="vertical-align:middle;margin-right:4px">error</span> {{vgError}}
  2538. </div>
  2539. <!-- 结果汇总卡片 -->
  2540. <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;margin-bottom:20px">
  2541. <!-- 脚本统计 -->
  2542. <div style="padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2543. <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
  2544. <span class="icon" style="color:var(--accent-color)">auto_stories</span>
  2545. <strong>分镜脚本</strong>
  2546. </div>
  2547. <p style="margin:0;font-size:24px;font-weight:600">{{vgScriptSegments.length}}</p>
  2548. <p style="margin:2px 0 0;font-size:12px;color:var(--text-secondary)">个场景片段</p>
  2549. </div>
  2550. <!-- 配图统计 -->
  2551. <div style="padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2552. <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
  2553. <span class="icon" style="color:#4CAF50">image</span>
  2554. <strong>AI 配图</strong>
  2555. </div>
  2556. <p style="margin:0;font-size:24px;font-weight:600">
  2557. {{vgImageResults.filter(r => r.status === 'done').length}} / {{vgImageResults.length}}
  2558. </p>
  2559. <p style="margin:2px 0 0;font-size:12px;color:var(--text-secondary)">张图片已生成</p>
  2560. </div>
  2561. <!-- 配音统计 -->
  2562. <div style="padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2563. <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
  2564. <span class="icon" style="color:#FF9800">record_voice_over</span>
  2565. <strong>语音配音</strong>
  2566. </div>
  2567. <p style="margin:0;font-size:24px;font-weight:600">
  2568. {{vgAudioUrls.length}} / {{vgScriptSegments.length}}
  2569. </p>
  2570. <p style="margin:2px 0 0;font-size:12px;color:var(--text-secondary)">段音频已合成</p>
  2571. </div>
  2572. </div>
  2573. <!-- 素材详情列表 -->
  2574. <div style="margin-bottom:20px">
  2575. <h4 style="margin:0 0 12px;font-size:14px">素材详情</h4>
  2576. <div style="display:flex;flex-direction:column;gap:10px">
  2577. <div *ngFor="let seg of vgScriptSegments; let i = index"
  2578. style="display:flex;gap:12px;padding:12px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2579. <div style="flex-shrink:0;width:120px;height:120px;border-radius:8px;overflow:hidden;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center">
  2580. <img *ngIf="vgImageResults[i]?.imageUrl" [src]="vgImageResults[i]?.imageUrl"
  2581. style="width:100%;height:100%;object-fit:cover">
  2582. <span *ngIf="!vgImageResults[i]?.imageUrl" class="icon" style="font-size:28px;color:var(--text-secondary)">image</span>
  2583. </div>
  2584. <div style="flex:1;min-width:0">
  2585. <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
  2586. <span style="background:var(--accent-color);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600">{{seg.id}}</span>
  2587. </div>
  2588. <p style="margin:0 0 6px;font-size:13px;color:var(--text-primary);line-height:1.5">{{seg.narration}}</p>
  2589. <div *ngIf="vgAudioUrls[i]?.audioUrl" style="margin-top:4px">
  2590. <audio [src]="vgAudioUrls[i]?.audioUrl" controls style="height:28px;width:100%;max-width:300px"></audio>
  2591. </div>
  2592. </div>
  2593. </div>
  2594. </div>
  2595. </div>
  2596. <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
  2597. <button class="btn" (click)="vgPrevStep()">
  2598. <span class="icon">arrow_back</span> 上一步
  2599. </button>
  2600. <div style="display:flex;gap:8px">
  2601. <button class="btn" (click)="vgReset()">
  2602. <span class="icon">restart_alt</span> 重新开始
  2603. </button>
  2604. <button class="btn btn-primary" (click)="vgComposite()" [disabled]="vgCompositing">
  2605. <span class="icon">{{vgCompositing ? 'hourglass_empty' : 'movie_creation'}}</span>
  2606. {{vgCompositing ? '合成中...' : vgFinalVideoUrl ? '重新合成视频' : '合成视频'}}
  2607. </button>
  2608. </div>
  2609. </div>
  2610. </div>
  2611. </section>
  2612. <!-- 使用说明 -->
  2613. <div class="vg-help-card" style="margin-top:24px;padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
  2614. <h4 style="margin:0 0 8px;display:flex;align-items:center;gap:6px;font-size:13px">
  2615. <span class="icon" style="font-size:16px">help_outline</span> 使用说明
  2616. </h4>
  2617. <div style="font-size:12px;color:var(--text-secondary);line-height:1.8">
  2618. <div><strong>步骤1:</strong>上传需要拆解的视频源文件(支持常见视频格式)</div>
  2619. <div><strong>步骤2:</strong>系统自动识别视频内容并整理文字稿</div>
  2620. <div><strong>步骤3:</strong>根据文字稿自动生成分镜脚本,可手动编辑调整</div>
  2621. <div><strong>步骤4:</strong>为每个分镜自动生成画面</div>
  2622. <div><strong>步骤5:</strong>选择声音模型,为旁白自动合成语音</div>
  2623. <div><strong>步骤6:</strong>将画面与配音合成为最终视频</div>
  2624. </div>
  2625. </div>
  2626. </section>
  2627. </ng-container>
  2628. </main>
  2629. </div>
  2630. </div>
  2631. <!-- 视频预览模态框 -->
  2632. <div class="preview-modal-overlay" *ngIf="showPreviewModal" (click)="closePreview()">
  2633. <div class="preview-modal" (click)="$event.stopPropagation()">
  2634. <div class="preview-modal-header">
  2635. <h3>{{previewVideoTitle}}</h3>
  2636. <button class="preview-close-btn" (click)="closePreview()">
  2637. <span class="icon">close</span>
  2638. </button>
  2639. </div>
  2640. <div class="preview-modal-body">
  2641. <div class="preview-player">
  2642. <video *ngIf="previewVideoUrl" controls autoplay [src]="previewVideoUrl" class="preview-video-element">
  2643. 您的浏览器不支持视频播放
  2644. </video>
  2645. <img *ngIf="!previewVideoUrl && previewVideoInfo?.poster" [src]="previewVideoInfo.poster" [alt]="previewVideoTitle" class="preview-video-element">
  2646. <div *ngIf="previewVideoInfo?.loading && !previewVideoUrl" style="padding:12px 0;color:var(--text-secondary);text-align:center;">
  2647. 正在获取可播放视频地址...
  2648. </div>
  2649. </div>
  2650. <div class="preview-info" *ngIf="previewVideoInfo">
  2651. <div class="preview-info-grid">
  2652. <div class="preview-info-item" *ngIf="previewVideoInfo.filename">
  2653. <span class="preview-info-label">文件名</span>
  2654. <span class="preview-info-value">{{previewVideoInfo.filename}}</span>
  2655. </div>
  2656. <div class="preview-info-item" *ngIf="previewVideoInfo.size">
  2657. <span class="preview-info-label">文件大小</span>
  2658. <span class="preview-info-value">{{previewVideoInfo.size | fileSize}}</span>
  2659. </div>
  2660. <div class="preview-info-item" *ngIf="previewVideoInfo.duration">
  2661. <span class="preview-info-label">时长</span>
  2662. <span class="preview-info-value">{{previewVideoInfo.duration | timeFormat}}</span>
  2663. </div>
  2664. <div class="preview-info-item" *ngIf="previewVideoInfo.resolution">
  2665. <span class="preview-info-label">分辨率</span>
  2666. <span class="preview-info-value">{{previewVideoInfo.resolution}}</span>
  2667. </div>
  2668. <div class="preview-info-item" *ngIf="previewVideoInfo.format">
  2669. <span class="preview-info-label">格式</span>
  2670. <span class="preview-info-value">{{previewVideoInfo.format | uppercase}}</span>
  2671. </div>
  2672. <div class="preview-info-item" *ngIf="previewVideoInfo.source">
  2673. <span class="preview-info-label">来源</span>
  2674. <span class="preview-info-value">{{previewVideoInfo.source === 'downloaded' ? '已下载' : previewVideoInfo.source === 'generated' ? 'AI生成' : previewVideoInfo.source === 'douyin' ? '抖音搜索' : '已上传'}}</span>
  2675. </div>
  2676. <div class="preview-info-item" *ngIf="previewVideoInfo.quality">
  2677. <span class="preview-info-label">画质</span>
  2678. <span class="preview-info-value">{{previewVideoInfo.quality}}</span>
  2679. </div>
  2680. <div class="preview-info-item" *ngIf="previewVideoInfo.cost !== undefined">
  2681. <span class="preview-info-label">费用</span>
  2682. <span class="preview-info-value">¥{{previewVideoInfo.cost}}</span>
  2683. </div>
  2684. <div class="preview-info-item" *ngIf="previewVideoInfo.created_at">
  2685. <span class="preview-info-label">创建时间</span>
  2686. <span class="preview-info-value">{{previewVideoInfo.created_at | date:'yyyy-MM-dd HH:mm'}}</span>
  2687. </div>
  2688. <div class="preview-info-item" *ngIf="previewVideoInfo.authorName">
  2689. <span class="preview-info-label">作者</span>
  2690. <span class="preview-info-value">{{previewVideoInfo.authorName}}</span>
  2691. </div>
  2692. <div class="preview-info-item" *ngIf="previewVideoInfo.playCount !== undefined">
  2693. <span class="preview-info-label">播放量</span>
  2694. <span class="preview-info-value">{{previewVideoInfo.playCount | numberFormat}}</span>
  2695. </div>
  2696. <div class="preview-info-item" *ngIf="previewVideoInfo.diggCount !== undefined">
  2697. <span class="preview-info-label">点赞数</span>
  2698. <span class="preview-info-value">{{previewVideoInfo.diggCount | numberFormat}}</span>
  2699. </div>
  2700. <div class="preview-info-item" *ngIf="previewVideoInfo.commentCount !== undefined">
  2701. <span class="preview-info-label">评论数</span>
  2702. <span class="preview-info-value">{{previewVideoInfo.commentCount | numberFormat}}</span>
  2703. </div>
  2704. <div class="preview-info-item" *ngIf="previewVideoInfo.shareCount !== undefined">
  2705. <span class="preview-info-label">分享数</span>
  2706. <span class="preview-info-value">{{previewVideoInfo.shareCount | numberFormat}}</span>
  2707. </div>
  2708. </div>
  2709. <div class="preview-description" *ngIf="previewVideoInfo.description">
  2710. <span class="preview-info-label">描述</span>
  2711. <p>{{previewVideoInfo.description}}</p>
  2712. </div>
  2713. <div class="preview-tags" *ngIf="previewVideoInfo.tags?.length > 0">
  2714. <span class="preview-info-label">标签</span>
  2715. <div class="preview-tag-list">
  2716. <span class="tag" *ngFor="let tag of previewVideoInfo.tags">#{{tag}}</span>
  2717. </div>
  2718. </div>
  2719. </div>
  2720. </div>
  2721. <div class="preview-modal-footer">
  2722. <button class="btn" (click)="closePreview()">
  2723. <span class="icon">close</span> 关闭
  2724. </button>
  2725. <button class="btn btn-primary" *ngIf="previewSourceVideo" (click)="downloadPreviewVideo()" [disabled]="previewSourceVideo.isDownloading">
  2726. <span class="icon">{{ previewSourceVideo.isDownloading ? 'hourglass_empty' : 'download' }}</span>
  2727. {{ previewSourceVideo.isDownloading ? '下载中 ' + previewSourceVideo.downloadProgress + '%' : '下载' }}
  2728. </button>
  2729. <a class="btn btn-primary" *ngIf="!previewSourceVideo && previewDownloadUrl" [href]="previewDownloadUrl" download>
  2730. <span class="icon">download</span> 下载
  2731. </a>
  2732. </div>
  2733. </div>
  2734. </div>
  2735. <!-- AI重塑模态框 — 多步骤流程 -->
  2736. <div class="remix-modal-overlay" *ngIf="showRemixModal" (click)="closeRemixModal()">
  2737. <div class="remix-modal" (click)="$event.stopPropagation()">
  2738. <div class="remix-modal-header">
  2739. <h3>
  2740. <span class="icon">auto_awesome</span>
  2741. AI重塑 — {{remixTargetVideo?.title}}
  2742. </h3>
  2743. <button class="preview-close-btn" (click)="closeRemixModal()" [disabled]="isRemixSubmitting">
  2744. <span class="icon">close</span>
  2745. </button>
  2746. </div>
  2747. <!-- 步骤指示器 -->
  2748. <div class="remix-stepper">
  2749. <div class="remix-step-item" *ngFor="let item of getActiveRemixSteps()"
  2750. [class.active]="remixStep === item.step"
  2751. [class.done]="remixStep > item.step">
  2752. <div class="remix-step-circle">{{remixStep > item.step ? '✓' : item.step}}</div>
  2753. <span class="remix-step-text">{{item.label}}</span>
  2754. </div>
  2755. </div>
  2756. <div class="remix-modal-body">
  2757. <!-- ===== Step 1: 提取文字稿 ===== -->
  2758. <div *ngIf="remixStep === 1">
  2759. <div class="remix-section">
  2760. <label class="remix-label">重塑模式</label>
  2761. <div class="remix-mode-grid">
  2762. <div class="remix-mode-card"
  2763. [class.active]="remixMode === 'standard'"
  2764. (click)="setRemixMode('standard')">
  2765. <div class="remix-mode-title">标准 AI重塑</div>
  2766. <div class="remix-mode-desc">基于文字稿、AI美化和分镜脚本,批量生成多个重塑片段并自动拼接。</div>
  2767. </div>
  2768. <div class="remix-mode-card"
  2769. [class.active]="remixMode === 'digital-human'"
  2770. (click)="setRemixMode('digital-human')">
  2771. <div class="remix-mode-title">数字人生成</div>
  2772. <div class="remix-mode-desc">上传人物形象图,复用原视频音频生成单条数字人口播视频。</div>
  2773. </div>
  2774. </div>
  2775. </div>
  2776. <ng-container *ngIf="remixMode === 'standard'">
  2777. <div class="remix-section">
  2778. <label class="remix-label">视频文字稿</label>
  2779. <!-- 提示:已有分镜数据可跳过 -->
  2780. <div class="remix-whisper-loaded" *ngIf="remixStoryboard.length > 0">
  2781. <span class="icon" style="color:var(--success-color)">check_circle</span>
  2782. 已自动加载 Whisper 处理结果({{remixStoryboard.length}} 个分镜片段),可直接跳到分镜脚本步骤。
  2783. </div>
  2784. <!-- 提示:已加载文字稿 -->
  2785. <div class="remix-whisper-loaded" *ngIf="remixTranscript && remixStoryboard.length === 0 && remixTargetVideo?.whisper">
  2786. <span class="icon" style="color:var(--accent-color)">description</span>
  2787. 已加载该视频的 Whisper 转录文字稿,可编辑后进入下一步。
  2788. </div>
  2789. <!-- Whisper 识别进度 -->
  2790. <div class="remix-whisper-progress-box" *ngIf="isWhisperRunning">
  2791. <div class="remix-stitch-banner">
  2792. <span class="icon spinning" style="color:var(--accent-color)">sync</span>
  2793. <span>{{whisperStatusText}}</span>
  2794. </div>
  2795. </div>
  2796. <!-- Whisper 识别完成提示 -->
  2797. <div class="remix-whisper-loaded" *ngIf="whisperStatusText.startsWith('✅') && !isWhisperRunning">
  2798. <span class="icon" style="color:var(--success-color)">check_circle</span>
  2799. {{whisperStatusText}}
  2800. </div>
  2801. <!-- 生成文字稿操作区(无文字稿且不在识别中时显示) -->
  2802. <div class="remix-no-transcript" *ngIf="!remixTranscript && !isWhisperRunning && !showManualTranscriptInput">
  2803. <div class="remix-no-transcript-icon">
  2804. <span class="icon" style="font-size:36px;color:var(--text-muted)">subtitles_off</span>
  2805. </div>
  2806. <div class="remix-no-transcript-text">该视频暂无文字稿,请选择生成方式</div>
  2807. <div class="remix-no-transcript-actions">
  2808. <button class="btn btn-primary" (click)="generateTranscript()">
  2809. <span class="icon">mic</span> 使用 Whisper 自动识别
  2810. </button>
  2811. <button class="btn" (click)="showManualTranscriptInput = true">
  2812. <span class="icon">edit_note</span> 手动输入文字稿
  2813. </button>
  2814. </div>
  2815. </div>
  2816. <!-- 文字稿输入/编辑区 -->
  2817. <textarea class="remix-textarea"
  2818. *ngIf="remixTranscript || showManualTranscriptInput || remixTargetVideo?.whisper"
  2819. [(ngModel)]="remixTranscript"
  2820. rows="10"
  2821. placeholder="请输入视频的文字稿内容..."></textarea>
  2822. </div>
  2823. </ng-container>
  2824. <ng-container *ngIf="remixMode === 'digital-human'">
  2825. <div class="remix-section">
  2826. <app-digital-human-form
  2827. [audioSourceOptions]="['tts', 'original']"
  2828. [showPortraitPipeline]="true"
  2829. [timbreOptions]="remixTimbreOptions"
  2830. [promptPresets]="digitalHumanPromptPresets"
  2831. [originalAudioVideoId]="remixTargetVideo?.id || ''"
  2832. [originalAudioLabel]="remixTargetVideo?.title || ''"
  2833. [submitting]="isRemixSubmitting"
  2834. submitButtonLabel="开始生成数字人视频"
  2835. (request)="onRemixDhRequest($event)">
  2836. </app-digital-human-form>
  2837. </div>
  2838. </ng-container>
  2839. </div>
  2840. <!-- ===== Step 2: AI美化 ===== -->
  2841. <div *ngIf="remixStep === 2">
  2842. <div class="remix-section">
  2843. <label class="remix-label">原始文字稿</label>
  2844. <div class="remix-original-text">{{remixTranscript}}</div>
  2845. </div>
  2846. <div class="remix-section">
  2847. <label class="remix-label">
  2848. <span class="icon" style="font-size:16px;vertical-align:middle">auto_fix_high</span>
  2849. AI美化后的文字稿
  2850. </label>
  2851. <div class="remix-hint" style="margin-bottom:8px">
  2852. 已自动去除口语化词汇、整理段落结构。可手动修改。
  2853. </div>
  2854. <textarea class="remix-textarea"
  2855. [(ngModel)]="remixBeautifiedTranscript"
  2856. rows="10"
  2857. placeholder="美化后的文字稿"></textarea>
  2858. </div>
  2859. </div>
  2860. <!-- ===== Step 3: 分镜脚本 ===== -->
  2861. <div *ngIf="remixStep === 3">
  2862. <div class="remix-section">
  2863. <label class="remix-label">选择风格</label>
  2864. <div class="remix-style-grid">
  2865. <div class="remix-style-card"
  2866. *ngFor="let style of remixStyleOptions"
  2867. [class.active]="remixStyle === style.id"
  2868. (click)="remixStyle = style.id; generateStoryboard()">
  2869. <span class="remix-style-name">{{style.name}}</span>
  2870. </div>
  2871. </div>
  2872. </div>
  2873. <div class="remix-section">
  2874. <label class="remix-label">分镜脚本 ({{remixStoryboard.length}} 个片段)</label>
  2875. <div class="remix-hint" style="margin-bottom:8px">每段约40-45字,可编辑提示词(Prompt)以调整画面。</div>
  2876. <div class="storyboard-list">
  2877. <div class="storyboard-card" *ngFor="let seg of remixStoryboard; let i = index">
  2878. <div class="storyboard-card-head">
  2879. <span class="storyboard-id">{{seg.id}}</span>
  2880. <span class="storyboard-act">{{seg.act}}</span>
  2881. <span class="storyboard-dur">{{seg.duration}}</span>
  2882. </div>
  2883. <div class="storyboard-narration">{{seg.narration}}</div>
  2884. <textarea class="remix-textarea storyboard-prompt"
  2885. [(ngModel)]="seg.prompt"
  2886. rows="2"
  2887. placeholder="中文视频提示词..."></textarea>
  2888. </div>
  2889. </div>
  2890. </div>
  2891. </div>
  2892. <!-- ===== Step 4: 生成参数 ===== -->
  2893. <div *ngIf="remixStep === 4">
  2894. <ng-container *ngIf="remixMode === 'standard'">
  2895. <div class="remix-section">
  2896. <label class="remix-label">生成参数配置</label>
  2897. <div class="remix-params">
  2898. <div class="remix-param-item">
  2899. <label class="remix-label">视频质量</label>
  2900. <select [(ngModel)]="remixQuality">
  2901. <option value="1080p">1080P</option>
  2902. <option value="pro">Pro 1080P</option>
  2903. <option value="720p">720P</option>
  2904. </select>
  2905. </div>
  2906. <div class="remix-param-item">
  2907. <label class="remix-label">每段时长</label>
  2908. <select [(ngModel)]="remixFrames">
  2909. <option [ngValue]="121">5秒 (121帧)</option>
  2910. <option [ngValue]="241">10秒 (241帧)</option>
  2911. </select>
  2912. </div>
  2913. <div class="remix-param-item">
  2914. <label class="remix-label">画面比例</label>
  2915. <select [(ngModel)]="remixAspectRatio">
  2916. <option value="16:9">16:9 横屏</option>
  2917. <option value="9:16">9:16 竖屏</option>
  2918. <option value="1:1">1:1 正方形</option>
  2919. <option value="4:3">4:3</option>
  2920. </select>
  2921. </div>
  2922. </div>
  2923. </div>
  2924. <div class="remix-cost">
  2925. <span class="icon">attach_money</span>
  2926. 预计生成:<strong>{{remixStoryboard.length}} 个片段</strong>
  2927. </div>
  2928. </ng-container>
  2929. </div>
  2930. <!-- ===== Step 5: 生成中 ===== -->
  2931. <div *ngIf="remixStep === 5">
  2932. <div class="remix-section">
  2933. <div class="remix-progress-section">
  2934. <div class="remix-progress-bar-container">
  2935. <div class="remix-progress-bar-fill" [style.width]="(remixTargetVideo?.remixProgress || 0) + '%'"></div>
  2936. </div>
  2937. <div class="remix-progress-text">
  2938. {{remixStatusText}} ({{remixTargetVideo?.remixProgress | number:'1.0-0'}}%)
  2939. </div>
  2940. </div>
  2941. <div class="remix-digital-human-progress-card" *ngIf="remixMode === 'digital-human'">
  2942. <div class="remix-hint">
  2943. {{remixDigitalHumanAudioSource === 'tts'
  2944. ? '正在使用上传形象图与合成语音生成数字人视频,请稍候。'
  2945. : '正在使用上传形象图与原视频提取音频生成数字人视频,请稍候。'}}
  2946. </div>
  2947. <div class="remix-digital-human-inline-preview" *ngIf="remixDigitalHumanImagePreviewUrl">
  2948. <img [src]="remixDigitalHumanImagePreviewUrl" [alt]="remixDigitalHumanImageName || '数字人形象图'">
  2949. <div>
  2950. <div class="remix-digital-human-name">{{remixDigitalHumanImageName}}</div>
  2951. <div class="remix-hint">当前模式:数字人生成 · {{remixDigitalHumanAudioSource === 'tts' ? '语音合成' : '原视频提取音频'}}</div>
  2952. </div>
  2953. </div>
  2954. </div>
  2955. <div class="storyboard-list" style="margin-top:16px">
  2956. <ng-container *ngIf="remixMode === 'standard'">
  2957. <div class="storyboard-card" *ngFor="let seg of remixStoryboard"
  2958. [class.generating]="seg.status === 'generating'"
  2959. [class.completed]="seg.status === 'completed'"
  2960. [class.failed]="seg.status === 'failed'">
  2961. <div class="storyboard-card-head">
  2962. <span class="storyboard-id">{{seg.id}}</span>
  2963. <span class="storyboard-status-icon">
  2964. <span class="icon" *ngIf="seg.status === 'pending'" style="color:var(--text-muted)">hourglass_empty</span>
  2965. <span class="icon" *ngIf="seg.status === 'generating'" style="color:var(--accent-color)">autorenew</span>
  2966. <span class="icon" *ngIf="seg.status === 'completed'" style="color:var(--success-color)">check_circle</span>
  2967. <span class="icon" *ngIf="seg.status === 'failed'" style="color:var(--error-color)">error</span>
  2968. </span>
  2969. <span class="storyboard-dur">{{seg.duration}}</span>
  2970. </div>
  2971. <div class="storyboard-narration">{{seg.narration}}</div>
  2972. <div class="storyboard-error" *ngIf="seg.error">{{seg.error}}</div>
  2973. </div>
  2974. </ng-container>
  2975. </div>
  2976. </div>
  2977. </div>
  2978. <!-- ===== Step 6: 结果汇总 ===== -->
  2979. <div *ngIf="remixStep === 6">
  2980. <div *ngIf="remixMode === 'digital-human' && remixFinalVideoUrl" class="remix-stitch-banner stitch-done">
  2981. <span class="icon" style="color:var(--success-color)">check_circle</span>
  2982. <span>数字人视频生成完成!</span>
  2983. <a [href]="remixFinalVideoUrl" target="_blank" class="storyboard-video-link" style="margin-left:auto">
  2984. <span class="icon">play_circle</span> 播放视频
  2985. </a>
  2986. </div>
  2987. <!-- 一键成片状态 -->
  2988. <div class="remix-stitch-banner" *ngIf="remixMode === 'standard' && (remixStitchStatus === 'stitching' || remixStitchStatus === 'polling')">
  2989. <span class="icon spinning" style="color:var(--accent-color)">sync</span>
  2990. <span>{{remixStatusText}}</span>
  2991. </div>
  2992. <div class="remix-stitch-banner stitch-done" *ngIf="remixMode === 'standard' && remixStitchStatus === 'done'">
  2993. <span class="icon" style="color:var(--success-color)">check_circle</span>
  2994. <span>视频拼接完成!</span>
  2995. <a [href]="remixStitchUrl" target="_blank" class="storyboard-video-link" style="margin-left:auto">
  2996. <span class="icon">play_circle</span> 播放完整视频
  2997. </a>
  2998. </div>
  2999. <div class="remix-stitch-banner stitch-failed" *ngIf="remixStitchStatus === 'failed'">
  3000. <span class="icon" style="color:var(--error-color)">warning</span>
  3001. <span>{{remixMode === 'digital-human' ? remixStitchError : '自动拼接失败:' + remixStitchError + '(各片段仍可单独查看)'}}</span>
  3002. </div>
  3003. <div class="remix-result-summary">
  3004. <div class="remix-result-stat success">
  3005. <span class="remix-result-num">{{remixResults.success}}</span>
  3006. <span class="remix-result-label">成功</span>
  3007. </div>
  3008. <div class="remix-result-stat failed" *ngIf="remixResults.failed > 0">
  3009. <span class="remix-result-num">{{remixResults.failed}}</span>
  3010. <span class="remix-result-label">失败</span>
  3011. </div>
  3012. <div class="remix-result-stat total">
  3013. <span class="remix-result-num">{{remixResults.total}}</span>
  3014. <span class="remix-result-label">总计</span>
  3015. </div>
  3016. </div>
  3017. <div class="storyboard-list" style="margin-top:16px">
  3018. <ng-container *ngIf="remixMode === 'standard'">
  3019. <div class="storyboard-card" *ngFor="let seg of remixStoryboard"
  3020. [class.completed]="seg.status === 'completed'"
  3021. [class.failed]="seg.status === 'failed'">
  3022. <div class="storyboard-card-head">
  3023. <span class="storyboard-id">{{seg.id}}</span>
  3024. <span class="icon" *ngIf="seg.status === 'completed'" style="color:var(--success-color)">check_circle</span>
  3025. <span class="icon" *ngIf="seg.status === 'failed'" style="color:var(--error-color)">error</span>
  3026. </div>
  3027. <div class="storyboard-narration">{{seg.narration}}</div>
  3028. <a *ngIf="seg.videoUrl" [href]="seg.videoUrl" target="_blank" class="storyboard-video-link">
  3029. <span class="icon">play_circle</span> 查看片段
  3030. </a>
  3031. <div class="storyboard-error" *ngIf="seg.error">{{seg.error}}</div>
  3032. </div>
  3033. </ng-container>
  3034. </div>
  3035. </div>
  3036. </div>
  3037. <!-- 底部操作按钮 -->
  3038. <div class="remix-modal-footer">
  3039. <!-- Step 1 -->
  3040. <ng-container *ngIf="remixStep === 1">
  3041. <button class="btn" (click)="closeRemixModal()">取消</button>
  3042. <button class="btn btn-accent" *ngIf="remixMode === 'standard' && remixStoryboard.length > 0" (click)="remixStep = 3">
  3043. <span class="icon">skip_next</span> 跳到分镜脚本
  3044. </button>
  3045. <button class="btn btn-primary"
  3046. *ngIf="remixMode === 'standard'"
  3047. (click)="goToRemixGenerationConfig()"
  3048. [disabled]="!remixTranscript.trim()">
  3049. 下一步:AI美化 <span class="icon">arrow_forward</span>
  3050. </button>
  3051. <div class="remix-footer-status" *ngIf="remixMode === 'digital-human'" style="font-size:12px;color:var(--text-muted)">
  3052. 完成表单后点击右下方「开始生成数字人视频」按钮
  3053. </div>
  3054. </ng-container>
  3055. <!-- Step 2 -->
  3056. <ng-container *ngIf="remixMode === 'standard' && remixStep === 2">
  3057. <button class="btn" (click)="remixStep = 1"><span class="icon">arrow_back</span> 上一步</button>
  3058. <button class="btn btn-primary" (click)="confirmBeautified()" [disabled]="!remixBeautifiedTranscript.trim()">
  3059. 下一步:分镜脚本 <span class="icon">arrow_forward</span>
  3060. </button>
  3061. </ng-container>
  3062. <!-- Step 3 -->
  3063. <ng-container *ngIf="remixMode === 'standard' && remixStep === 3">
  3064. <button class="btn" (click)="remixStep = 2"><span class="icon">arrow_back</span> 上一步</button>
  3065. <button class="btn btn-primary" (click)="confirmStoryboard()" [disabled]="remixStoryboard.length === 0">
  3066. 下一步:生成参数 <span class="icon">arrow_forward</span>
  3067. </button>
  3068. </ng-container>
  3069. <!-- Step 4(仅标准重塑模式)-->
  3070. <ng-container *ngIf="remixStep === 4 && remixMode === 'standard'">
  3071. <button class="btn" (click)="remixStep = 3"><span class="icon">arrow_back</span> 上一步</button>
  3072. <button class="btn btn-primary" (click)="startBatchGeneration()">
  3073. <span class="icon">auto_awesome</span> 开始生成 ({{remixStoryboard.length}} 个片段)
  3074. </button>
  3075. </ng-container>
  3076. <!-- Step 5 -->
  3077. <ng-container *ngIf="remixStep === 5">
  3078. <div class="remix-footer-status">{{remixStatusText}}</div>
  3079. </ng-container>
  3080. <!-- Step 6 -->
  3081. <ng-container *ngIf="remixStep === 6">
  3082. <button class="btn" (click)="closeRemixModal()">关闭</button>
  3083. <button class="btn btn-primary" (click)="finishRemix()">
  3084. <span class="icon">visibility</span> 查看生成结果
  3085. </button>
  3086. </ng-container>
  3087. </div>
  3088. </div>
  3089. </div>
  3090. <!-- AI重塑模式选择弹窗 -->
  3091. <div class="remix-modal-overlay remix-mode-chooser-overlay" *ngIf="showRemixModeChooser" (click)="closeRemixModeChooser()">
  3092. <div class="remix-mode-chooser" (click)="$event.stopPropagation()">
  3093. <div class="remix-mode-chooser__header">
  3094. <h3>
  3095. <span class="icon">auto_awesome</span>
  3096. 选择重塑方式
  3097. </h3>
  3098. <button type="button" class="remix-mode-chooser__close" (click)="closeRemixModeChooser()" aria-label="关闭">
  3099. <span class="icon">close</span>
  3100. </button>
  3101. </div>
  3102. <p class="remix-mode-chooser__subtitle" *ngIf="remixModeChooserVideo">
  3103. 当前视频:{{remixModeChooserVideo.title}}
  3104. </p>
  3105. <div class="remix-mode-chooser__grid">
  3106. <button type="button" class="remix-mode-chooser__card" (click)="selectRemixMode('standard')">
  3107. <div class="remix-mode-chooser__card-title">标准AI重塑</div>
  3108. <div class="remix-mode-chooser__card-desc">基于文字稿、AI美化和分镜脚本,批量生成多个重塑片段并自动拼接。</div>
  3109. </button>
  3110. <button type="button" class="remix-mode-chooser__card" (click)="selectRemixMode('digital-human')">
  3111. <div class="remix-mode-chooser__card-title">数字人生成</div>
  3112. <div class="remix-mode-chooser__card-desc">上传人物形象图,复用原视频内容生成单条数字人口播视频。</div>
  3113. </button>
  3114. </div>
  3115. </div>
  3116. </div>
  3117. <!-- 视频上传模态框 -->
  3118. <div class="remix-modal-overlay" *ngIf="showUploadModal" (click)="closeUploadModal()">
  3119. <div class="remix-modal" style="max-width:560px" (click)="$event.stopPropagation()">
  3120. <div class="remix-modal-header">
  3121. <h3>
  3122. <span class="icon">upload</span> 上传视频
  3123. </h3>
  3124. <button class="icon-btn" (click)="closeUploadModal()" [disabled]="isUploading">
  3125. <span class="icon">close</span>
  3126. </button>
  3127. </div>
  3128. <div class="remix-modal-body">
  3129. <!-- 文件选择区域 -->
  3130. <div style="border:2px dashed var(--border-color);border-radius:12px;padding:32px;text-align:center;margin-bottom:16px;transition:border-color 0.2s ease,background-color 0.2s ease"
  3131. [style.border-color]="uploadFileName ? 'var(--accent-color)' : 'var(--border-color)'"
  3132. [style.background]="uploadFileName ? 'var(--accent-color-alpha, rgba(33,150,243,0.05))' : 'transparent'">
  3133. <input type="file" id="videoFileInput" accept="video/mp4,video/quicktime,video/x-msvideo,video/webm,video/x-matroska,.mp4,.mov,.avi,.webm,.mkv"
  3134. (change)="onFileSelected($event)" style="display:none" [disabled]="isUploading">
  3135. <div *ngIf="!uploadFileName">
  3136. <span class="icon" style="font-size:48px;color:var(--text-secondary);display:block;margin-bottom:8px">cloud_upload</span>
  3137. <p style="margin:0 0 8px;color:var(--text-primary);font-size:15px;font-weight:500">点击选择视频文件</p>
  3138. <p style="margin:0;color:var(--text-secondary);font-size:13px">支持 MP4、MOV、AVI、WebM、MKV 格式,最大 500MB</p>
  3139. <label for="videoFileInput" class="btn btn-primary" style="margin-top:12px;cursor:pointer;display:inline-flex">
  3140. <span class="icon">folder_open</span> 选择文件
  3141. </label>
  3142. </div>
  3143. <div *ngIf="uploadFileName">
  3144. <span class="icon" style="font-size:36px;color:var(--accent-color);display:block;margin-bottom:8px">movie</span>
  3145. <p style="margin:0 0 4px;color:var(--text-primary);font-size:14px;font-weight:500">{{uploadFileName}}</p>
  3146. <p *ngIf="uploadFile" style="margin:0;color:var(--text-secondary);font-size:12px">
  3147. 文件大小: {{(uploadFile.size / 1024 / 1024).toFixed(1)}} MB
  3148. </p>
  3149. <label for="videoFileInput" class="btn" style="margin-top:8px;cursor:pointer;display:inline-flex;font-size:12px" *ngIf="!isUploading">
  3150. <span class="icon" style="font-size:14px">swap_horiz</span> 更换文件
  3151. </label>
  3152. </div>
  3153. </div>
  3154. <!-- 视频信息表单 -->
  3155. <div style="display:flex;flex-direction:column;gap:12px">
  3156. <div class="form-field">
  3157. <label>视频标题</label>
  3158. <input type="text" [(ngModel)]="uploadTitle" placeholder="输入视频标题" [disabled]="isUploading">
  3159. </div>
  3160. <div class="form-field">
  3161. <label>描述</label>
  3162. <textarea [(ngModel)]="uploadDescription" placeholder="输入视频描述(可选)" rows="2"
  3163. style="resize:vertical;min-height:48px" [disabled]="isUploading"></textarea>
  3164. </div>
  3165. <div class="form-field">
  3166. <label>标签</label>
  3167. <input type="text" [(ngModel)]="uploadTags" placeholder="用逗号分隔多个标签(可选)" [disabled]="isUploading">
  3168. </div>
  3169. </div>
  3170. <!-- 上传进度 -->
  3171. <div *ngIf="isUploading" style="margin-top:16px">
  3172. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
  3173. <span style="font-size:13px;color:var(--text-primary)">正在上传...</span>
  3174. <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{uploadProgress}}%</span>
  3175. </div>
  3176. <div class="progress-bar" style="height:8px">
  3177. <div class="progress-fill" [style.width]="uploadProgress + '%'"
  3178. [class.progress-done]="uploadProgress === 100"
  3179. style="transition:width 0.3s"></div>
  3180. </div>
  3181. <p *ngIf="uploadProgress === 100" style="margin:8px 0 0;font-size:12px;color:var(--text-secondary)">
  3182. 文件已传输完成,正在处理...
  3183. </p>
  3184. </div>
  3185. <!-- 错误信息 -->
  3186. <div *ngIf="uploadError" style="margin-top:12px;padding:10px 14px;background:var(--error-bg, rgba(244,67,54,0.08));border-radius:8px;color:var(--error-color);font-size:13px;display:flex;align-items:center;gap:6px">
  3187. <span class="icon" style="font-size:16px">error</span>
  3188. {{uploadError}}
  3189. </div>
  3190. </div>
  3191. <div class="remix-modal-footer">
  3192. <button class="btn" (click)="closeUploadModal()" [disabled]="isUploading">取消</button>
  3193. <button class="btn btn-primary" (click)="uploadVideo()" [disabled]="!uploadFile || isUploading">
  3194. <span class="icon">{{isUploading ? 'hourglass_empty' : 'upload'}}</span>
  3195. {{isUploading ? '上传中...' : '开始上传'}}
  3196. </button>
  3197. </div>
  3198. </div>
  3199. </div>
  3200. <!-- 历史详情模态框 -->
  3201. <div class="remix-modal-overlay" *ngIf="showHistoryDetailModal" (click)="closeHistoryDetail()">
  3202. <div class="modal remix-modal" style="max-width:800px;max-height:85vh;overflow-y:auto" (click)="$event.stopPropagation()">
  3203. <div class="modal-header">
  3204. <h3>
  3205. <span class="icon">history</span>
  3206. 重塑详情 — {{historyDetailRecord?.original_video_title || historyDetailRecord?.keyword}}
  3207. </h3>
  3208. <button class="icon-btn" (click)="closeHistoryDetail()">
  3209. <span class="icon">close</span>
  3210. </button>
  3211. </div>
  3212. <div class="modal-body" style="padding:16px">
  3213. <div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px;padding:12px;background:var(--bg-tertiary);border-radius:8px">
  3214. <div *ngIf="historyDetailRecord?.styleName"><strong>风格:</strong>{{historyDetailRecord?.styleName}}</div>
  3215. <div *ngIf="historyDetailRecord?.duration"><strong>片段:</strong>{{historyDetailRecord?.duration}}</div>
  3216. <div *ngIf="historyDetailRecord?.quality"><strong>画质:</strong>{{historyDetailRecord?.quality}}</div>
  3217. <div><strong>费用:</strong>¥{{historyDetailRecord?.cost || 0}}</div>
  3218. <div><strong>状态:</strong>
  3219. <span class="status-badge" [class]="getStatusColor(historyDetailRecord?.status || '')">
  3220. {{getStatusText(historyDetailRecord?.status || '')}}
  3221. </span>
  3222. </div>
  3223. <div><strong>时间:</strong>{{historyDetailRecord?.created_at | date:'yyyy-MM-dd HH:mm'}}</div>
  3224. </div>
  3225. <div class="history-primary-video" *ngIf="historyDetailPrimaryVideoUrl">
  3226. <div class="history-primary-video-head">
  3227. <h4>
  3228. <span class="icon" style="font-size:18px;vertical-align:middle">play_circle</span>
  3229. 生成结果
  3230. </h4>
  3231. <a [href]="historyDetailPrimaryVideoUrl" target="_blank" class="btn btn-sm">
  3232. <span class="icon" style="font-size:14px">open_in_new</span> 新窗口播放
  3233. </a>
  3234. </div>
  3235. <video controls [src]="historyDetailPrimaryVideoUrl" class="history-primary-video-player"></video>
  3236. </div>
  3237. <div *ngIf="historyDetailLoading" class="history-detail-loading">
  3238. 正在加载详情视频与片段数据...
  3239. </div>
  3240. <h4 style="margin-bottom:12px">
  3241. <span class="icon" style="font-size:18px;vertical-align:middle">video_library</span>
  3242. 视频片段 ({{historyDetailSegments.length}})
  3243. </h4>
  3244. <div *ngIf="historyDetailSegments.length === 0 && !historyDetailLoading" style="text-align:center;padding:24px;color:var(--text-secondary)">
  3245. <span class="icon" style="font-size:36px;display:block;margin-bottom:8px">info</span>
  3246. {{historyDetailPrimaryVideoUrl ? '当前记录暂无独立片段数据,已展示主视频结果' : '暂无片段数据(该记录可能未关联重塑数据)'}}
  3247. </div>
  3248. <div class="storyboard-list" style="gap:12px">
  3249. <div class="storyboard-card" *ngFor="let seg of historyDetailSegments"
  3250. [class.completed]="seg.status === 'completed'"
  3251. [class.failed]="seg.status === 'failed'"
  3252. style="margin-bottom:12px">
  3253. <div class="storyboard-card-head">
  3254. <span class="storyboard-id">{{seg.id}}</span>
  3255. <span class="status-badge" [class]="seg.status === 'completed' ? 'badge-success' : seg.status === 'failed' ? 'badge-danger' : 'badge-secondary'">
  3256. {{seg.status === 'completed' ? '成功' : seg.status === 'failed' ? '失败' : seg.status}}
  3257. </span>
  3258. </div>
  3259. <div *ngIf="seg.narration" style="font-size:13px;color:var(--text-secondary);margin:6px 0">
  3260. {{seg.narration}}
  3261. </div>
  3262. <div *ngIf="seg.videoUrl" style="margin-top:8px">
  3263. <video controls [src]="seg.videoUrl" style="width:100%;max-height:240px;border-radius:6px;background:#000"></video>
  3264. </div>
  3265. <div *ngIf="seg.videoUrl" style="margin-top:6px">
  3266. <a [href]="seg.videoUrl" target="_blank" class="btn btn-sm" style="font-size:12px">
  3267. <span class="icon" style="font-size:14px">open_in_new</span> 新窗口播放
  3268. </a>
  3269. </div>
  3270. <div *ngIf="!seg.videoUrl && seg.status === 'failed'" style="color:var(--error-color);font-size:13px;margin-top:6px">
  3271. <span class="icon" style="font-size:14px">error</span> 生成失败
  3272. </div>
  3273. </div>
  3274. </div>
  3275. </div>
  3276. <div class="modal-footer" style="padding:12px 16px;border-top:1px solid var(--border-color);display:flex;justify-content:flex-end">
  3277. <button class="btn" (click)="closeHistoryDetail()">关闭</button>
  3278. </div>
  3279. </div>
  3280. </div>
  3281. <!-- 浮动 mini-pill:最近一次生成(P3)。
  3282. 与 AI 助手 FAB 共用右下角,通过 .has-assistant 类把 pill 抬高到 FAB 上方 -->
  3283. <div class="modal-overlay viral-analysis-overlay vf-modal-backdrop" *ngIf="isViralAnalysisOpen" (click)="closeViralAnalysis()">
  3284. <div class="modal viral-analysis-modal vf-modal" (click)="$event.stopPropagation()">
  3285. <div class="modal-header">
  3286. <h3><span class="icon">insights</span> 爆款分析</h3>
  3287. <button class="icon-btn" type="button" (click)="closeViralAnalysis()" [disabled]="isAnalyzingViralVideo">
  3288. <span class="icon">close</span>
  3289. </button>
  3290. </div>
  3291. <div class="modal-body viral-analysis-body">
  3292. <div class="viral-analysis-loading" *ngIf="isAnalyzingViralVideo">
  3293. <div class="spinner"></div>
  3294. <strong>正在分析视频结构</strong>
  3295. <p>会结合视频信息和评论样本,提炼可复用的选题与脚本框架。</p>
  3296. </div>
  3297. <div class="monitor-error" *ngIf="viralAnalysisError && !isAnalyzingViralVideo">{{viralAnalysisError}}</div>
  3298. <ng-container *ngIf="currentViralAnalysis as insight">
  3299. <section class="viral-analysis-hero">
  3300. <div>
  3301. <span class="viral-analysis-kicker">来源视频</span>
  3302. <h4>{{insight.videoSnapshot.desc || viralAnalysisSourceTitle}}</h4>
  3303. <p>
  3304. {{insight.videoSnapshot.authorName || '未知作者'}}
  3305. · 置信度:{{insight.confidence === 'high' ? '高' : insight.confidence === 'medium' ? '中' : '低'}}
  3306. · 评论 {{insight.commentsSnapshot.length || 0}} 条
  3307. <ng-container *ngIf="insight.repliesSnapshot?.length"> · 回复 {{insight.repliesSnapshot?.length}} 条</ng-container>
  3308. <ng-container *ngIf="insight.transcript"> · 已使用字幕</ng-container>
  3309. </p>
  3310. </div>
  3311. <span class="status-badge">{{insight.source === 'monitor' ? '博主监测' : '视频发现'}}</span>
  3312. </section>
  3313. <section class="viral-analysis-section viral-analysis-section--summary">
  3314. <h4>爆点判断</h4>
  3315. <p>{{insight.analysis.summary}}</p>
  3316. </section>
  3317. <section class="viral-analysis-section viral-analysis-section--evidence">
  3318. <div class="viral-section-heading">
  3319. <div>
  3320. <span class="viral-analysis-kicker">分析依据</span>
  3321. <h4>证据与逐字稿</h4>
  3322. </div>
  3323. <span class="vf-status-pill" *ngIf="insight.transcriptJob">
  3324. {{viralTranscriptStatusLabel(insight.transcriptJob)}}
  3325. </span>
  3326. </div>
  3327. <p>
  3328. 互动:播放 {{insight.videoSnapshot.playCount || 0}} / 点赞 {{insight.videoSnapshot.diggCount || 0}} / 评论 {{insight.videoSnapshot.commentCount || 0}} / 分享 {{insight.videoSnapshot.shareCount || 0}}
  3329. </p>
  3330. <p>
  3331. 样本:评论 {{insight.commentsSnapshot.length || 0}} 条
  3332. <ng-container *ngIf="insight.repliesSnapshot?.length"> / 回复 {{insight.repliesSnapshot?.length}} 条</ng-container>
  3333. / 逐字稿:{{insight.transcript ? '已使用' : '未提供,按结构推断'}}
  3334. </p>
  3335. <p *ngIf="insight.transcriptJob">
  3336. 转写任务:{{viralTranscriptStatusLabel(insight.transcriptJob)}}
  3337. <ng-container *ngIf="insight.transcriptJob.stageLabel"> / {{insight.transcriptJob.stageLabel}}</ng-container>
  3338. <ng-container *ngIf="insight.transcriptJob.orderId"> / {{insight.transcriptJob.orderId}}</ng-container>
  3339. </p>
  3340. <div class="viral-transcript-progress" *ngIf="insight.transcriptJob && viralTranscriptIsProcessing(insight.transcriptJob)">
  3341. <div class="viral-transcript-progress__bar">
  3342. <div class="viral-transcript-progress__fill" [style.width]="(insight.transcriptJob.progress || 0) + '%'"></div>
  3343. </div>
  3344. <span>{{insight.transcriptJob.progress || 0}}%</span>
  3345. </div>
  3346. <div class="viral-transcript-alert" *ngIf="viralTranscriptShouldWarn(insight.transcriptJob)">
  3347. <strong>{{viralTranscriptPrimaryMessage(insight.transcriptJob)}}</strong>
  3348. <p *ngFor="let warning of (insight.transcriptJob?.warnings || [])">{{warning}}</p>
  3349. <p *ngIf="insight.transcriptJob?.errorMessage">{{insight.transcriptJob?.errorMessage}}</p>
  3350. </div>
  3351. <div class="viral-transcript-actions" *ngIf="!insight.transcript">
  3352. <button class="vf-btn vf-btn-primary vf-btn-lg viral-transcript-action--primary" type="button" *ngIf="canStartViralTranscript(insight)"
  3353. (click)="startViralTranscript()" [disabled]="viralTranscriptBusy">
  3354. <span class="icon">subtitles</span>
  3355. {{viralTranscriptBusy ? '提交中...' : (insight.transcriptJob ? '重新提交转写' : '补逐字稿')}}
  3356. </button>
  3357. <button class="vf-btn vf-btn-lg viral-transcript-action--refresh" type="button" *ngIf="insight.transcriptJob && insight.transcriptJob.status !== 'completed'"
  3358. (click)="refreshViralTranscript()" [disabled]="viralTranscriptBusy">
  3359. <span class="icon">refresh</span>
  3360. {{viralTranscriptBusy ? '自动跟踪中' : '刷新转写结果'}}
  3361. </button>
  3362. </div>
  3363. </section>
  3364. <div class="viral-analysis-grid">
  3365. <section class="viral-analysis-section">
  3366. <h4>钩子类型</h4>
  3367. <p>{{insight.analysis.hookType || '待判断'}}</p>
  3368. </section>
  3369. <section class="viral-analysis-section">
  3370. <h4>开头方式</h4>
  3371. <p>{{insight.analysis.openingPattern || '待判断'}}</p>
  3372. </section>
  3373. <section class="viral-analysis-section">
  3374. <h4>核心冲突</h4>
  3375. <p>{{insight.analysis.conflict || '待判断'}}</p>
  3376. </section>
  3377. <section class="viral-analysis-section">
  3378. <h4>用户情绪</h4>
  3379. <p>{{insight.analysis.audienceEmotion || '待判断'}}</p>
  3380. </section>
  3381. <section class="viral-analysis-section">
  3382. <h4>评论触发点</h4>
  3383. <p>{{insight.analysis.commentTrigger || '暂无可用评论触发点'}}</p>
  3384. </section>
  3385. </div>
  3386. <section class="viral-analysis-section">
  3387. <h4>内容节奏</h4>
  3388. <p>{{insight.analysis.contentRhythm}}</p>
  3389. </section>
  3390. <section class="viral-analysis-section">
  3391. <h4>可复用脚本框架</h4>
  3392. <p>{{insight.analysis.reusableFrame}}</p>
  3393. </section>
  3394. <section class="viral-analysis-section" *ngIf="insight.transcript">
  3395. <h4>字幕/口播依据</h4>
  3396. <p>{{insight.transcript | slice:0:220}}{{insight.transcript.length > 220 ? '...' : ''}}</p>
  3397. </section>
  3398. <section class="viral-analysis-section" *ngIf="insight.analysis.reusableAngles.length">
  3399. <h4>同款选题方向</h4>
  3400. <div class="viral-angle-list">
  3401. <span class="viral-angle" *ngFor="let angle of insight.analysis.reusableAngles">{{angle}}</span>
  3402. </div>
  3403. </section>
  3404. <section class="viral-analysis-section" *ngIf="insight.analysis.evidenceRefs.length">
  3405. <h4>证据引用</h4>
  3406. <p *ngFor="let ref of insight.analysis.evidenceRefs">{{ref}}</p>
  3407. </section>
  3408. <section class="viral-analysis-section" *ngIf="insight.analysis.riskNotes.length || insight.confidence !== 'high'">
  3409. <h4>注意事项</h4>
  3410. <p *ngIf="insight.confidence !== 'high'">当前证据不足以判定为高置信,建议补充逐字稿或更多匹配评论后再作为正式脚本。</p>
  3411. <p *ngFor="let note of insight.analysis.riskNotes">{{note}}</p>
  3412. </section>
  3413. </ng-container>
  3414. </div>
  3415. <div class="modal-footer viral-analysis-footer">
  3416. <button class="vf-btn vf-btn-lg" type="button" (click)="closeViralAnalysis()" [disabled]="isAnalyzingViralVideo">关闭</button>
  3417. <button class="vf-btn vf-btn-lg" type="button" (click)="saveCurrentViralAnalysisToTopics()" [disabled]="!currentViralAnalysis || isAnalyzingViralVideo">
  3418. 保存到选题池
  3419. </button>
  3420. <button class="vf-btn vf-btn-primary vf-btn-lg" type="button" (click)="useViralAnalysisForTopicVideo()" [disabled]="!currentViralAnalysis || isAnalyzingViralVideo">
  3421. 进入主题生视频
  3422. </button>
  3423. </div>
  3424. </div>
  3425. </div>
  3426. <div class="modal-overlay daily-report-overlay" *ngIf="isDailyReportOpen" (click)="closeDailyReport()">
  3427. <div class="modal daily-report-modal" (click)="$event.stopPropagation()">
  3428. <div class="modal-header daily-report-header">
  3429. <div>
  3430. <span class="daily-report-eyebrow">博主热点监测</span>
  3431. <h3><span class="icon">summarize</span> 监测日报</h3>
  3432. </div>
  3433. <button class="icon-btn" type="button" (click)="closeDailyReport()">
  3434. <span class="icon">close</span>
  3435. </button>
  3436. </div>
  3437. <div class="modal-body daily-report-body" *ngIf="currentDailyReport as report">
  3438. <section class="daily-report-summary">
  3439. <div>
  3440. <span>日报日期</span>
  3441. <strong>{{report.date}}</strong>
  3442. </div>
  3443. <div>
  3444. <span>监测博主</span>
  3445. <strong>{{getDailyReportMetric(report, 'authorCount', monitorAuthors.length)}}</strong>
  3446. </div>
  3447. <div>
  3448. <span>覆盖作品</span>
  3449. <strong>{{getDailyReportMetric(report, 'worksCount', report.newWorks.length)}}</strong>
  3450. </div>
  3451. <div>
  3452. <span>推荐选题</span>
  3453. <strong>{{report.topTopics.length}}</strong>
  3454. </div>
  3455. </section>
  3456. <section class="daily-report-section">
  3457. <div class="daily-report-section-head">
  3458. <h4>值得关注的作品</h4>
  3459. <span>优先覆盖已加载作品的不同博主,再按互动热度补充</span>
  3460. </div>
  3461. <article class="daily-work" *ngFor="let work of report.newWorks | slice:0:6">
  3462. <div class="daily-work__head">
  3463. <strong>{{$any(work).authorName || '监测博主'}}</strong>
  3464. <span>互动分 {{($any(work).score || 0) | number:'1.0-0'}}</span>
  3465. </div>
  3466. <p>{{$any(work).desc || $any(work).id}}</p>
  3467. <div class="daily-work__stats">
  3468. <span>点赞 {{$any(work).statistics?.digg_count || 0}}</span>
  3469. <span>评论 {{$any(work).statistics?.comment_count || 0}}</span>
  3470. <span>分享 {{$any(work).statistics?.share_count || 0}}</span>
  3471. </div>
  3472. <p *ngIf="$any(work).commentSamples?.length">评论关注:{{$any(work).commentSamples[0]}}</p>
  3473. </article>
  3474. </section>
  3475. <section class="daily-report-section">
  3476. <div class="daily-report-section-head">
  3477. <h4>可保存选题</h4>
  3478. <span>可直接进入主题生视频继续生产</span>
  3479. </div>
  3480. <article class="daily-topic" *ngFor="let topic of report.topTopics">
  3481. <div>
  3482. <strong>{{topic.title}}</strong>
  3483. <p>{{topic.angle}}</p>
  3484. </div>
  3485. <button class="daily-report-topic-action" type="button" (click)="openDailyReportTopic(topic)">进入主题生视频</button>
  3486. </article>
  3487. </section>
  3488. <section class="daily-report-section">
  3489. <h4>明日建议</h4>
  3490. <p>{{report.calibrationPrompt}}</p>
  3491. </section>
  3492. </div>
  3493. <div class="modal-footer daily-report-footer">
  3494. <button class="vf-btn vf-btn-secondary" type="button" (click)="closeDailyReport()">关闭</button>
  3495. <button class="vf-btn vf-btn-primary" type="button" (click)="saveDailyReportTopics()" [disabled]="!currentDailyReport?.topTopics?.length">
  3496. 保存选题到选题池
  3497. </button>
  3498. </div>
  3499. </div>
  3500. </div>
  3501. <app-recent-task-pill
  3502. *ngIf="auth.isLoggedIn"
  3503. [task]="currentRunningTask"
  3504. [hidden]="pillHidden"
  3505. [class.has-assistant]="!assistantExpanded"
  3506. (open)="setCurrentTab('tasks')"
  3507. (dismiss)="dismissCurrentRunningTask()">
  3508. </app-recent-task-pill>
  3509. <!-- 全局 AI 创作助手(永远在场,跨 tab 持久化) -->
  3510. <app-assistant
  3511. *ngIf="auth.isLoggedIn"
  3512. (suggestionPick)="onAssistantSuggestion($event)"
  3513. (expandedChange)="assistantExpanded = $event">
  3514. </app-assistant>
  3515. <app-auth-required-modal
  3516. [open]="authModalOpen"
  3517. [featureName]="authModalFeature"
  3518. (close)="authModalOpen = false"
  3519. (login)="openAuth('login')"
  3520. (register)="openAuth('register')">
  3521. </app-auth-required-modal>
  3522. <!-- Toast 通知 -->
  3523. <div class="toast-container" *ngIf="toastVisible" [ngClass]="['toast-' + toastType]" (click)="toastVisible = false">
  3524. <span class="icon toast-icon">
  3525. {{toastType === 'success' ? 'check_circle' : toastType === 'error' ? 'error' : toastType === 'warn' ? 'warning' : 'info'}}
  3526. </span>
  3527. <span class="toast-text">{{toastMessage}}</span>
  3528. <span class="icon toast-close">close</span>
  3529. </div>