Bläddra i källkod

feat: 生成视频功能

sheldon 3 veckor sedan
förälder
incheckning
10860c56fa

+ 3 - 5
src/components/AgentPage/index.tsx

@@ -40,12 +40,10 @@ export default function Index({ agentId }: IProps) {
     fetchMyEntList()
   }, [])
   
-  // 自定义背景样式
-  const bgImageStyle = {
-    backgroundImage: `url(${agent?.avatarUrl})`,
-  };
+  
+
   return (
-    <PageCustom styleBg={bgImageStyle}>
+    <PageCustom styleBg={agent?.avatarUrl} >
       <NavBarNormal leftColumn={Logo}></NavBarNormal>
       <View className="blur-rounded-container">
         {(!!agent) ? <SummaryBar isVisitor={false} agent={agent}></SummaryBar> : <></>}

+ 64 - 0
src/components/AvatarMedia/index.tsx

@@ -0,0 +1,64 @@
+import { Image, Video, VideoProps } from "@tarojs/components";
+// import style from "../index.module.less";
+import { useEffect, useRef } from "react";
+import Taro from "@tarojs/taro";
+
+interface Props {
+  source: string;
+  className: string;
+}
+
+export const AvatarMedia = ({ source, className }: Props) => {
+  
+  const videoRef = useRef<React.ComponentType<VideoProps>|null>(null);
+  const videoContext = useRef<Taro.VideoContext|null>(null);
+  const videoId = useRef(`video-${Math.random().toString(36).substr(2, 9)}`);
+
+  const videoErrorCallback = (e) =>  {
+    console.log('Video error info:')
+    console.log(e.detail)
+ }
+
+ // 处理 ios 上无法循环播放的 bug
+ const handleOnEnded = () => {
+  if(videoContext.current){
+    videoContext.current.seek(0)
+    videoContext.current.play()
+  }
+ }
+
+
+  useEffect(() => {
+    if (videoRef.current) {
+      videoContext.current = Taro.createVideoContext(videoId.current, this);
+    }
+  }, []);
+
+  if (source.lastIndexOf('.mp4') > -1) {
+    return (
+      <Video
+        id={videoId.current}
+        ref={videoRef}
+        controls={false}
+        showCenterPlayBtn={false}
+        loop={true}
+        muted={true}
+        autoplay
+        onEnded={handleOnEnded}
+        objectFit="cover"
+        className={className}
+        onError={videoErrorCallback}
+        // src="https://cdn.wehome.cn/cmn/mp4/3/META-H8UKWHWU-YAUTZH7ECGRDC57FD3NI3-CUGVCS8M-CD.mp4"
+        src={source}
+      />
+    );  
+  }
+
+  return (
+    <Image
+      mode="widthFix"
+      className={className}
+      src={source}
+    />
+  );
+};

+ 1 - 1
src/components/page-custom/index.tsx

@@ -8,7 +8,7 @@ interface Props {
   bgColor?: string;
   isflex?: boolean; // 有横向滚动的scroll-view 容器不能包裹在 flex comlumn 布局中
   style?: React.CSSProperties;
-  styleBg?: React.CSSProperties;
+  styleBg?: string;
   paddingTop?: number;
   fullPage?: boolean;
   onClick?: (e:any)=> void

+ 2 - 0
src/components/page-wrapper/index.module.less

@@ -20,6 +20,8 @@
   top: 0;
   right: 0;
   bottom: 0;
+  width: 100%;
+  height: 100%;
   background-size: 100%!important;
   background-repeat: no-repeat!important;
   // background-color: var(--color-bg-primary)!important;

+ 28 - 5
src/components/page-wrapper/index.tsx

@@ -1,20 +1,43 @@
 /**
  * 用于 page 根目录的样式等设置
  */
-import { View, Image } from "@tarojs/components";
+import { View, Video } from "@tarojs/components";
 import React from "react";
 import pageStyle from "./index.module.less";
 import { GlobalModal } from '@/components/GlobalModal/index';
 interface Props {
   children?: React.ReactChild | React.ReactChild[];
   style?: React.CSSProperties;
-  styleBg?: React.CSSProperties;
+  styleBg?: string;
   onClick?: (e:any)=> void
 }
 
 const Index: React.FC<Props> = ({ children, styleBg, style, onClick }) => {
-  // 是否提示非移动端打开小程序
-  // const desktopPopupVisible = useAppStore((state)=> state.desktopPopupTips)
+  // 自定义背景样式
+  
+  
+  const bgImageStyle = {
+    backgroundImage: `url(${styleBg})`,
+  };
+
+  const renderBg = () => {
+    if(!styleBg) {
+      return <></>
+    }
+    if(styleBg.lastIndexOf('.mp4') > -1){
+      return <Video
+        controls={false}
+        showCenterPlayBtn={false}
+        loop={true}
+        muted={true}
+        autoplay
+        objectFit="cover"
+        className={pageStyle.bg}
+        src={styleBg}
+      />
+    }
+    return <View className={pageStyle.bg} style={bgImageStyle}></View>
+  }
   //  盖在背景上面的是视口高度的 渐变色
   const handleClick = (e:any)=> {
     onClick && onClick(e)
@@ -25,7 +48,7 @@ const Index: React.FC<Props> = ({ children, styleBg, style, onClick }) => {
       {/* cover 背景图覆盖从上至下渐变 */}
       <View className={`global-linear-gradient-bg ${pageStyle.bgVerticalGradient}`}></View>
       {/* bg 背景图 */}
-      <View className={pageStyle.bg} style={styleBg}></View>
+      {renderBg()}
       <View className="relative z-0 h-full w-full" style={style}>{children}</View>
       <GlobalModal></GlobalModal>
     </View>

+ 31 - 0
src/pages/agent-avatars/index.module.less

@@ -52,4 +52,35 @@
   height: 28px;
   background-color: rgba(#FFF, .6);
   border-bottom-left-radius: 12px;
+}
+
+.videoContainer {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.playBtn{
+  width: 11px;
+  height: 11px;
+  background: url(https://cdn.wehome.cn/cmn/png/0/META-H8UK0IWU-9NNPJOLLD1MU95DE0NMA3-J2FYMV2M-T41.png) no-repeat;
+  background-size: 100%;
+}
+.durationStatus{
+  display: flex;
+  align-items: center;
+  padding: 0 8px;
+  gap: 2px;
+  position: absolute;
+  right: 8px;
+  bottom: 8px;
+  z-index: 1;
+  border-radius: 30px;
+  font-size: 10px;
+  line-height: 18px;
+  color: white;
+  background-color: rgba(black, .45);
 }

+ 87 - 69
src/pages/agent-avatars/index.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useRef, useState } from "react";
-import { Image, View, ScrollView } from "@tarojs/components";
-
+import { Image, View, ScrollView, Video } from "@tarojs/components";
+import { formatSeconds } from "@/utils/index";
 import PageCustom from "@/components/page-custom/index";
 import NavBarNormal from "@/components/NavBarNormal/index";
 
@@ -26,14 +26,17 @@ export default function Index() {
   const [scrollTop, setScrollTop] = useState(0);
   const scrollPositionRef = useRef(0);
 
-  const { showModal } = useModalStore()
+  const { showModal } = useModalStore();
 
-  const fetcher = async ([_url, {pageIndex, pageSize}]) => {
+  const fetcher = async ([_url, { pageIndex, pageSize }]) => {
     const res = await fetchMyAvatars({ pageIndex, pageSize });
     return res.data;
   };
 
-  const { list, loadMore, mutate } = useLoadMoreInfinite<TAvatarItem[]>(createKey('fetchMyAvatars'), fetcher);
+  const { list, loadMore, mutate } = useLoadMoreInfinite<TAvatarItem[]>(
+    createKey("fetchMyAvatars"),
+    fetcher
+  );
 
   const [current, setCurrent] = useState<TAvatarItem | null>(null);
 
@@ -44,105 +47,120 @@ export default function Index() {
     }
 
     Taro.showLoading();
-    const result = await editAgentAvatar(
-      agent.agentId,
-      item.avatarId,
-      false,
-    );
-    await fetchAgent(agent.agentId)
+    const result = await editAgentAvatar(agent.agentId, item.avatarId, false);
+    await fetchAgent(agent.agentId);
     Taro.hideLoading();
     setCurrent(item);
-    if(isSuccess(result.status)){
+    if (isSuccess(result.status)) {
       Taro.navigateBack();
     }
   };
   const onScrollToLower = () => {
     loadMore();
-    console.log('lower')
+    console.log("lower");
   };
   const handleCreate = () => {
     uploadAndNavToGenNewAvatar();
   };
 
-  const handleDelete = async (e:any, avatar: TAvatarItem) => {
-    e.stopPropagation()
+  const handleDelete = async (e: any, avatar: TAvatarItem) => {
+    e.stopPropagation();
     showModal({
       content: <>确认删除该形象?</>,
       async onConfirm() {
-        const response = await deleteAvatar(avatar.avatarId)
-        if(isSuccess(response.status)){
-          mutate()
+        const response = await deleteAvatar(avatar.avatarId);
+        if (isSuccess(response.status)) {
+          mutate();
         }
-      }
-    })
-    
-  }
+      },
+    });
+  };
 
+  const renderMedia = (avatar: TAvatarItem) => {
+    if (avatar.isVideo) {
+      return (
+        <View className={style.videoContainer}>
+          <Video
+            controls={false}
+            showCenterPlayBtn={false}
+            loop={true}
+            muted={true}
+            objectFit="cover"
+            src={avatar.avatarUrl}
+            className="w-full h-full"
+          />
+            <View className={style.durationStatus}>
+              <View className={style.playBtn}></View>
+              <View>{formatSeconds(Math.round(avatar.videoSeconds))}</View>
+            </View>
+          )}
+        </View>
+      );
+    }
+    return <Image src={avatar.avatarUrl} mode="widthFix" className="w-full" />;
+  };
 
-  const renderList = ()=> {
-    if(!list.length){
-      return <>
-        <EmptyData type={'search'}></EmptyData>
-      </>
+  const renderList = () => {
+    if (!list.length) {
+      return (
+        <>
+          <EmptyData type={"search"}></EmptyData>
+        </>
+      );
     }
-    
+
     return list.map((avatar) => {
       const isCurrent = agent?.avatarUrl === avatar.avatarUrl;
       return (
         <View
-          className={
-            `${isCurrent
-              ? style.gridItemActived
-              : style.gridItem}`
-            
-          }
+          className={`${isCurrent ? style.gridItemActived : style.gridItem}`}
           onClick={() => handleClick(avatar)}
         >
-          {!isCurrent && <View className={style.deleteButton} onClick={(e)=> handleDelete(e, avatar)}>
-            <IconDeleteGray16/>
-          </View>}
-          <Image
-            src={avatar.avatarUrl}
-            mode="widthFix"
-            className="w-full"
-          />
+          {!isCurrent && (
+            <View
+              className={style.deleteButton}
+              onClick={(e) => handleDelete(e, avatar)}
+            >
+              <IconDeleteGray16 />
+            </View>
+          )}
+
+          {renderMedia(avatar)}
         </View>
       );
-    })
-  }
-
-  
+    });
+  };
 
   return (
     <PageCustom>
       <NavBarNormal>历史形象</NavBarNormal>
       <View className={style.container}>
-      <ScrollView
-        scrollY
-        onScrollToLower={onScrollToLower}
-        scrollTop={scrollTop}
-        onScroll={(e)=> {
-          scrollPositionRef.current = e.detail.scrollTop
-        }}
-        style={{
-          flex: 1,
-          height: "100%", // 高度自适应
-        }}
-      >
-        <View className="w-full p-16 pb-120">
-          <View className={style.grid}>
-            <View className={style.gridItem} onClick={handleCreate}>
-              <View className={style.icon}>
-                <IconPlusBig></IconPlusBig>
-              </View>
-              <View className="pt-8 text-12 leading-20 text-gray-45">
-                创建新形象
+        <ScrollView
+          scrollY
+          onScrollToLower={onScrollToLower}
+          scrollTop={scrollTop}
+          onScroll={(e) => {
+            scrollPositionRef.current = e.detail.scrollTop;
+          }}
+          style={{
+            flex: 1,
+            height: "100%", // 高度自适应
+          }}
+        >
+          <View className="w-full p-16 pb-120">
+            <View className={style.grid}>
+              <View className={style.gridItem} onClick={handleCreate}>
+                <View className={style.icon}>
+                  <IconPlusBig></IconPlusBig>
+                </View>
+                <View className="pt-8 text-12 leading-20 text-gray-45">
+                  创建新形象
+                </View>
               </View>
+              {renderList()}
             </View>
-            {renderList()}
           </View>
-        </View>
-      </ScrollView>
+        </ScrollView>
       </View>
     </PageCustom>
   );

+ 101 - 0
src/pages/agent-gen/components/step/GenAvatarVideo.tsx

@@ -0,0 +1,101 @@
+import { View, Image } from "@tarojs/components";
+import React, { useEffect, useState } from "react";
+import style from "./index.module.less";
+import IconStarColor from "@/components/icon/icon-star-color";
+import useSWR from 'swr';
+import Taro from "@tarojs/taro";
+interface IProps {
+  prev: () => void;
+  next: () => void;
+  setPickedAvatar: (pickedAvatar: TAvatarItem)=> void
+  taskId: string|number
+}
+export default React.memo(function Index({ prev, next, taskId, setPickedAvatar }: IProps) {
+  const [currentSwiperIndex, setCurrentSwiperIndex] = useState(0);
+  const [avatars, setAvatars] = useState<TAvatarItem[]>([]);
+  const [shouldPoll, setShouldPoll] = useState(false);
+  
+  
+  const goNext = () => {
+    const pickedAvatar = avatars[currentSwiperIndex]
+    setPickedAvatar(pickedAvatar)
+    console.log(pickedAvatar)
+    next()
+  }
+  
+  Taro.hideLoading()
+
+  const { data } = useSWR(
+    shouldPoll ? `genAvatarVideo${taskId}` : null,
+    ()=> getUploadedAvatarStatus(taskId),
+    {
+      revalidateOnFocus: false,
+      refreshInterval: shouldPoll ? 3000 : 0,
+      refreshWhenHidden: false,
+      refreshWhenOffline: false,
+      revalidateOnMount: false
+    }
+  );
+
+  useEffect(() => {
+    setShouldPoll(true);
+    return () => {
+      setShouldPoll(false);
+    };
+  }, []);
+
+  useEffect(()=> {
+    if(data?.data.status === 'success'){
+      setAvatars(data.data?.data.reverse())
+      setShouldPoll(false);
+    }
+    if(data?.data.status === 'process_fail'){
+      setShouldPoll(false);
+    }
+    return ()=> {
+      
+    }
+  }, [data])
+
+
+  const genVideo = () => {
+
+  }
+
+  const renderProgressStatus = () => {
+    if(data?.data.status === 'process_fail'){
+      return <View className="gradient-text">AI生成失败</View>
+    }
+    if(data?.data.status === 'success'){
+      return <View className="gradient-text">AI生成成功</View>
+    }
+    return <View className="text-gray-500">AI生中</View>
+  }
+
+
+  
+
+  return (
+    <View>
+      <View className={style.pickContainer}>
+        <View className={`${style.pickAvatarCard} ${style.pickGenCard}`}>
+          <View className="flex items-center gap-4">
+            <IconStarColor></IconStarColor>{" "}
+            {renderProgressStatus()}
+          </View>
+        </View>
+      </View>
+
+      <View className="bottom-bar">
+        <View className="grid grid-cols-2 gap-8 px-20 py-12">
+          <View className={`button-rounded`} onClick={prev}>
+            上一步
+          </View>
+          <View className={`button-rounded primary ${avatars.length ? '' : 'opacity-20'}`} onClick={()=> goNext()}>
+            使用这张
+          </View>
+        </View>
+      </View>
+    </View>
+  );
+});

+ 48 - 11
src/pages/agent-gen/components/step/StepPick.tsx

@@ -2,9 +2,10 @@ import { View, Swiper, SwiperItem, Image } from "@tarojs/components";
 import React, { useEffect, useState } from "react";
 import style from "./index.module.less";
 import IconStarColor from "@/components/icon/icon-star-color";
-import { getUploadedAvatarStatus, TAvatarItem } from '@/service/storage'
+import { getUploadedAvatarStatus, genAvatarVideo, type TAvatarItem } from '@/service/storage'
 import useSWR from 'swr';
 import Taro from "@tarojs/taro";
+import { isSuccess } from "@/utils";
 interface IProps {
   prev: () => void;
   next: () => void;
@@ -15,7 +16,8 @@ export default React.memo(function Index({ prev, next, taskId, setPickedAvatar }
   const [currentSwiperIndex, setCurrentSwiperIndex] = useState(0);
   const [avatars, setAvatars] = useState<TAvatarItem[]>([]);
   const [shouldPoll, setShouldPoll] = useState(false);
-  
+  const [videoGen, setVideoGen] = useState(false);
+  const [currentTaskId, setCurrentTaskId] = useState<string|number>(taskId);
   
   const goNext = () => {
     const pickedAvatar = avatars[currentSwiperIndex]
@@ -26,9 +28,9 @@ export default React.memo(function Index({ prev, next, taskId, setPickedAvatar }
   
   Taro.hideLoading()
 
-  const { data } = useSWR(
-    shouldPoll ? `getUploadedAvatarStatus${taskId}` : null,
-    ()=> getUploadedAvatarStatus(taskId),
+  const { data, mutate } = useSWR(
+    shouldPoll ? `getUploadedAvatarStatus${currentTaskId}` : null,
+    ()=> getUploadedAvatarStatus(currentTaskId),
     {
       revalidateOnFocus: false,
       refreshInterval: shouldPoll ? 3000 : 0,
@@ -50,20 +52,37 @@ export default React.memo(function Index({ prev, next, taskId, setPickedAvatar }
       setAvatars(data.data?.data.reverse())
       setShouldPoll(false);
     }
+    if(data?.data.status === 'process_fail'){
+      setShouldPoll(false);
+    }
     return ()=> {
       
     }
   }, [data])
+
+
+  const genVideo = async () => {
+    const a  = avatars[currentSwiperIndex]
+    if(!a.avatarUrl){
+      return 
+    }
+    const avatarUrl = a.avatarUrl
+    setVideoGen(true)
+    // 清空 useSWR 缓存
+    mutate(undefined, false); 
+    setAvatars([]);
+    const response = await genAvatarVideo({avatarUrl})
+    if(isSuccess(response.status)){
+      setCurrentTaskId(response.data.taskId)
+      setShouldPoll(true)
+    }
+  }
   
 
   const onSwiperChange = (e: any) => {
     const i = e.detail.current;
     setCurrentSwiperIndex(i);
-  };
-
-
-
-  
+  };  
 
   const renderSwipers = () => {
     const renderIndicator = (currentIndex: number) => {
@@ -119,15 +138,30 @@ export default React.memo(function Index({ prev, next, taskId, setPickedAvatar }
     );
   };
 
+  
+  
+  const renderProcessingStatus = () => {
+    const targetText = !videoGen ? 'AI' : '视频'
+      
+    if(data?.data.status === 'process_fail'){
+      return <View className="gradient-text">{`${targetText}生成失败`}</View>
+    }
+    if(data?.data.status === 'success'){
+      return <View className="gradient-text">{`${targetText}生成成功`}</View>
+    }
+    return <View className="gradient-text">{`${targetText}生成中`}</View>
+  }
+
   const renderContent = ()=> {
     if(avatars.length){
+      
       return renderSwipers()
     }
 
     return <View className={`${style.pickAvatarCard} ${style.pickGenCard}`}>
       <View className="flex items-center gap-4">
         <IconStarColor></IconStarColor>{" "}
-        <View className="gradient-text">AI生成中</View>
+        {renderProcessingStatus()}
       </View>
     </View>
   }
@@ -145,6 +179,9 @@ export default React.memo(function Index({ prev, next, taskId, setPickedAvatar }
           <View className={`button-rounded`} onClick={prev}>
             上一步
           </View>
+          <View className={`button-rounded`} onClick={genVideo}>
+            生成微视频
+          </View>
           <View className={`button-rounded primary ${avatars.length ? '' : 'opacity-20'}`} onClick={()=> goNext()}>
             使用这张
           </View>

+ 3 - 3
src/pages/agent-gen/components/step/StepStart.tsx

@@ -45,7 +45,7 @@ export default React.memo(function StepStart({ next, setTaskId }: IProps) {
     const response = await uploadAvatar({
       aiGenerated: true,
       avatarUrl: currentAvatarUrl,
-      description: value,
+      description: '',
     })
     Taro.hideLoading();
     if(isSuccess(response.status)){
@@ -64,14 +64,14 @@ export default React.memo(function StepStart({ next, setTaskId }: IProps) {
           <Image className={style.startCard} src={currentAvatarUrl} mode="widthFix"></Image>
         </View>
       </View>
-      <View className="mb-24">
+      {/* <View className="mb-24">
         <View className="text-14 font-medium text-black mb-10">创意描述<Text className="text-12 text-gray-45">(非必填)</Text></View>
         <WemetaTextarea
           value={value}
           onInput={handleInput}
           placeholder="描述你想要生成的画面和动作。例如:职场精英在点头微笑"
         />
-      </View>
+      </View> */}
       <View className="bottom-bar">
         <View className="px-20 py-12">
           <View className={`button-rounded-big gap-4 ${style.startGenButton}`} onClick={handleClick}>

+ 1 - 1
src/pages/agent-gen/components/step/index.module.less

@@ -50,7 +50,7 @@
   align-items: center;
   background: linear-gradient(162.45deg, rgba(206, 49, 250, 0.1) 1.4%, rgba(49, 124, 250, 0.2) 100.04%);
   background-size: auto 400%;
-  animation: gradientMove 4s linear infinite;
+  // animation: gradientMove 4s linear infinite;
 }
 
 @keyframes gradientMove {

+ 18 - 6
src/pages/agent/components/AgentSetting/components/AgentCard/index.tsx

@@ -7,6 +7,8 @@ import WemetaRadio from '@/components/WemetaRadio'
 import { useAgentStore } from "@/store/agentStore";
 import { editAgentChatBg } from "@/service/agent";
 import { isSuccess } from "@/utils";
+import { fetchMyAvatars } from "@/service/storage";
+import { AvatarMedia } from "@/components/AvatarMedia";
 
 export default () => {
   const {agent, fetchAgent} = useAgentStore()
@@ -24,7 +26,14 @@ export default () => {
       fetchAgent(agent.agentId); // 刷新智能体信息  
     }
   }
-  const handleClick = () => {
+  const handleClick = async () => {
+    const response = await fetchMyAvatars({ pageIndex: 1, pageSize: 2 });
+    if(isSuccess(response.status)){
+      if(response.data.data.length > 0){
+        Taro.navigateTo({url: '/pages/agent-avatars/index'})
+        return
+      }
+    }
     if (agent?.avatarUrl) {
       return;
     }
@@ -32,6 +41,11 @@ export default () => {
     
   };
 
+  const handleChangeAvatar = async (e:any) => {
+    e.stopPropagation();
+    Taro.navigateTo({url: '/pages/agent-avatars/index'})
+  };
+
   const renderContent = () => {
     if (!agent?.avatarUrl) {
       return (
@@ -45,11 +59,9 @@ export default () => {
     }
 
     return (
-      <View className="relative">
-        <Image src={agent.avatarUrl} mode="widthFix" className={style.card} />
-        <View className={style.changeButton} onClick={()=> {
-          Taro.navigateTo({url: '/pages/agent-avatars/index'})
-        }}>
+      <View className="relative overflow-hidden w-full h-full">
+        <AvatarMedia source={agent.avatarUrl} className={style.card} />
+        <View className={style.changeButton} onClick={handleChangeAvatar}>
           更换形象
         </View>
       </View>

+ 26 - 6
src/pages/chat/index.tsx

@@ -1,4 +1,4 @@
-import { View, ScrollView } from "@tarojs/components";
+import { View, ScrollView, Video } from "@tarojs/components";
 import NavBarNormal from "@/components/NavBarNormal/index";
 import PageCustom from "@/components/page-custom/index";
 import Taro, { useRouter, useUnload } from "@tarojs/taro";
@@ -191,15 +191,35 @@ export default function Index() {
     );
   };
 
-  // 自定义背景样式
-  const bgImageStyle = {
-    backgroundImage: `url(${agent?.avatarUrl})`,
-  };
+  
+
+  const renderTopBg = ()=> {
+    if(!agent?.avatarUrl || !!!agent?.enabledChatBg){
+      return <></>;
+    }
+    if(agent.avatarUrl.lastIndexOf(".mp4") > -1){
+      return <Video
+        controls={false}
+        showCenterPlayBtn={false}
+        loop={true}
+        muted={true}
+        autoplay
+        objectFit="cover"
+        className={style.topBg}
+        src={agent.avatarUrl}
+      />
+    }
+    // 自定义背景样式
+    const bgImageStyle = {
+      backgroundImage: `url(${agent?.avatarUrl})`,
+    };
+    return <View className={style.topBg} style={bgImageStyle}></View>
+  }
 
   return (
     <PageCustom fullPage style={{ overflow: "hidden" }}>
       <NavBarNormal leftColumn={renderNavLeft}></NavBarNormal>
-      {(!!agent?.enabledChatBg) ? <View className={style.topBg} style={bgImageStyle}></View> : <></>}
+      {renderTopBg()}
       <View
         className="flex flex-col w-full h-screen relative z-10"
         style={{ marginTop: `${marginTopOffset}px` }}

+ 2 - 7
src/pages/profile/index.tsx

@@ -90,14 +90,9 @@ export default function Profile() {
       </View>
     );
   };
-  // 自定义背景样式
-  const bgImageStyle = {
-    // background: `linear-gradient(rgba(242, 244, 245, 0) 0%, rgba(242, 244, 245, .8) 80% , rgba(242, 244, 245,1) 100%), url(${customBgImg})`,
-    // backgroundImage: `url(https://cdn.wehome.cn/cmn/png/53/META-H8UKWHWU-2JNUAG2BARJF55VHU9QS3-YBQGHDAM-IW.png)`,
-    backgroundImage: `url(${agentProfile?.avatarUrl})`,
-  };
+  
   return (
-    <PageCustom styleBg={bgImageStyle}>
+    <PageCustom styleBg={agentProfile?.avatarUrl}>
       <View className="w-full">
         <NavBarNormal
           leftColumn={renderNavBarLeft}

+ 9 - 0
src/service/storage.ts

@@ -13,6 +13,8 @@ export type TAvatarItem = {
   avatarLogo: string,
   avatarUrl: string,
   isOriginal: boolean
+  isVideo: boolean
+  videoSeconds: number
 }
 // 上传形象--并生成微视频
 export type TUploadAvatarResponse = {
@@ -30,6 +32,13 @@ export const uploadAvatar = (data: {
   return request.post<TUploadAvatarResponse>(`${bluebookAiAgent}api/v1/my/avatar`, data)
 }
 
+// 根据指定形象归生成微视频
+export const genAvatarVideo = (data: {
+  "avatarUrl": string,
+}) => {
+  return request.post<TUploadAvatarResponse>(`${bluebookAiAgent}api/v1/my/avatar/4video`, data)
+}
+
 // 获取上传形象任务的处理状态
 export const getUploadedAvatarStatus = (taskId: string|number) => {
   return request.get<TUploadAvatarResponse>(`${bluebookAiAgent}api/v1/my/avatar/task/${taskId}`)