0225067 4 miesięcy temu
rodzic
commit
98ad8f5d3a
100 zmienionych plików z 2300 dodań i 15 usunięć
  1. 41 1
      src/app/app-routing.module.ts
  2. 4 0
      src/app/app.component.ts
  3. 14 8
      src/app/tab1/tab1.page.html
  4. 78 0
      src/app/tab1/tab1.page.scss
  5. 378 5
      src/app/tab1/tab1.page.ts
  6. 7 0
      src/app/tabs/tabs-routing.module.ts
  7. 5 1
      src/app/tabs/tabs.page.html
  8. 2 0
      src/assets/icon/camera.svg
  9. BIN
      src/assets/images/666.jpg
  10. BIN
      src/assets/images/BackGround.png
  11. BIN
      src/assets/images/girlsSmile1.png
  12. BIN
      src/assets/images/home.png
  13. 14 0
      src/assets/index.html
  14. BIN
      src/assets/models/age_gender_model-shard1
  15. 0 0
      src/assets/models/age_gender_model-weights_manifest.json
  16. BIN
      src/assets/models/face_expression_model-shard1
  17. 0 0
      src/assets/models/face_expression_model-weights_manifest.json
  18. BIN
      src/assets/models/face_landmark_68_model-shard1
  19. 0 0
      src/assets/models/face_landmark_68_model-weights_manifest.json
  20. BIN
      src/assets/models/face_landmark_68_tiny_model-shard1
  21. 0 0
      src/assets/models/face_landmark_68_tiny_model-weights_manifest.json
  22. BIN
      src/assets/models/face_recognition_model-shard1
  23. 0 0
      src/assets/models/face_recognition_model-shard2
  24. 0 0
      src/assets/models/face_recognition_model-weights_manifest.json
  25. BIN
      src/assets/models/mtcnn_model-shard1
  26. 0 0
      src/assets/models/mtcnn_model-weights_manifest.json
  27. BIN
      src/assets/models/ssd_mobilenetv1_model-shard1
  28. 0 0
      src/assets/models/ssd_mobilenetv1_model-shard2
  29. 0 0
      src/assets/models/ssd_mobilenetv1_model-weights_manifest.json
  30. BIN
      src/assets/models/tiny_face_detector_model-shard1
  31. 0 0
      src/assets/models/tiny_face_detector_model-weights_manifest.json
  32. 0 0
      src/assets/script/face-api.min.js
  33. 298 0
      src/assets/script/scripts.js
  34. 2 0
      src/face-api-js-starter-main/.gitignore
  35. 3 0
      src/face-api-js-starter-main/.vscode/settings.json
  36. 16 0
      src/face-api-js-starter-main/package.json
  37. 0 0
      src/face-api-js-starter-main/public/face-api.min.js
  38. BIN
      src/face-api-js-starter-main/public/images/girlsSmile1.png
  39. BIN
      src/face-api-js-starter-main/public/images/girlsSmile2.png
  40. BIN
      src/face-api-js-starter-main/public/images/girlsSmile3.png
  41. BIN
      src/face-api-js-starter-main/public/images/girlsSmile4.png
  42. BIN
      src/face-api-js-starter-main/public/images/oldMan1.png
  43. BIN
      src/face-api-js-starter-main/public/images/oldMan2.png
  44. BIN
      src/face-api-js-starter-main/public/images/oldManSerious1.png
  45. BIN
      src/face-api-js-starter-main/public/images/oldManSerious2.png
  46. BIN
      src/face-api-js-starter-main/public/images/oldManSmiling.png
  47. BIN
      src/face-api-js-starter-main/public/images/oldWoman.png
  48. BIN
      src/face-api-js-starter-main/public/images/waterfall1.png
  49. BIN
      src/face-api-js-starter-main/public/images/waterfall2.png
  50. BIN
      src/face-api-js-starter-main/public/images/youngman1.png
  51. BIN
      src/face-api-js-starter-main/public/images/youngman2.png
  52. 14 0
      src/face-api-js-starter-main/public/index.html
  53. BIN
      src/face-api-js-starter-main/public/models/age_gender_model-shard1
  54. 0 0
      src/face-api-js-starter-main/public/models/age_gender_model-weights_manifest.json
  55. BIN
      src/face-api-js-starter-main/public/models/face_expression_model-shard1
  56. 0 0
      src/face-api-js-starter-main/public/models/face_expression_model-weights_manifest.json
  57. BIN
      src/face-api-js-starter-main/public/models/face_landmark_68_model-shard1
  58. 0 0
      src/face-api-js-starter-main/public/models/face_landmark_68_model-weights_manifest.json
  59. BIN
      src/face-api-js-starter-main/public/models/face_landmark_68_tiny_model-shard1
  60. 0 0
      src/face-api-js-starter-main/public/models/face_landmark_68_tiny_model-weights_manifest.json
  61. BIN
      src/face-api-js-starter-main/public/models/face_recognition_model-shard1
  62. 0 0
      src/face-api-js-starter-main/public/models/face_recognition_model-shard2
  63. 0 0
      src/face-api-js-starter-main/public/models/face_recognition_model-weights_manifest.json
  64. BIN
      src/face-api-js-starter-main/public/models/mtcnn_model-shard1
  65. 0 0
      src/face-api-js-starter-main/public/models/mtcnn_model-weights_manifest.json
  66. BIN
      src/face-api-js-starter-main/public/models/ssd_mobilenetv1_model-shard1
  67. 0 0
      src/face-api-js-starter-main/public/models/ssd_mobilenetv1_model-shard2
  68. 0 0
      src/face-api-js-starter-main/public/models/ssd_mobilenetv1_model-weights_manifest.json
  69. BIN
      src/face-api-js-starter-main/public/models/tiny_face_detector_model-shard1
  70. 0 0
      src/face-api-js-starter-main/public/models/tiny_face_detector_model-weights_manifest.json
  71. 298 0
      src/face-api-js-starter-main/public/scripts.js
  72. 30 0
      src/face-api-js-starter-main/readme.md
  73. 4 0
      src/face-api-js-starter-main/server.js
  74. 17 0
      src/modules/aigc/agent/agent-shige/agent-shige-routing.module.ts
  75. 20 0
      src/modules/aigc/agent/agent-shige/agent-shige.module.ts
  76. 41 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.html
  77. 0 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.scss
  78. 17 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.spec.ts
  79. 91 0
      src/modules/aigc/agent/agent-shige/agent-shige.page.ts
  80. 16 0
      src/modules/aigc/aigc-routing.module.ts
  81. 13 0
      src/modules/aigc/aigc.module.ts
  82. 17 0
      src/modules/aigc/chat/chat-routing.module.ts
  83. 22 0
      src/modules/aigc/chat/chat.data.md
  84. 20 0
      src/modules/aigc/chat/chat.module.ts
  85. 24 0
      src/modules/aigc/chat/chat.page.html
  86. 0 0
      src/modules/aigc/chat/chat.page.scss
  87. 17 0
      src/modules/aigc/chat/chat.page.spec.ts
  88. 119 0
      src/modules/aigc/chat/chat.page.ts
  89. 147 0
      src/modules/aigc/chat/class-rxjs-chat-completion.ts
  90. 103 0
      src/modules/aigc/chat/class-test-chat-completion.ts
  91. 30 0
      src/modules/babylon/README.md
  92. 12 0
      src/modules/babylon/pages/case-babylon/README.md
  93. 17 0
      src/modules/babylon/pages/case-babylon/case-babylon-routing.module.ts
  94. 20 0
      src/modules/babylon/pages/case-babylon/case-babylon.module.ts
  95. 6 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.html
  96. 43 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.scss
  97. 17 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.spec.ts
  98. 171 0
      src/modules/babylon/pages/case-babylon/case-babylon.page.ts
  99. 79 0
      src/modules/contact/README.md
  100. 13 0
      src/modules/contact/chat/chat-routing.module.ts

+ 41 - 1
src/app/app-routing.module.ts

@@ -1,11 +1,51 @@
 import { NgModule } from '@angular/core';
 import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
+import { authGuard } from 'src/modules/user/auth.guard';
 
 const routes: Routes = [
   {
     path: '',
     loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
-  }
+  },
+
+  {
+    path: 'user',
+    loadChildren: () => import('../modules/user/user.module').then(m => m.UserModule)
+  },
+  {
+    path:'study',
+    canActivate:[authGuard],//路由守卫
+    children:[
+      {
+          path: 'parse',
+          loadChildren: () => import('../modules/study/case-parse/case-parse.module').then( m => m.CaseParsePageModule)
+      },
+      {
+          path: 'results',
+          loadChildren: () => import('../modules/study/results/results.module').then( m => m.ResultsPageModule)
+      },
+      {
+        path: 'camera',
+        loadChildren: () => import('../modules/study/camera-page/camera-page.module').then( m => m.CameraPagePageModule)
+      },
+    ]
+  }, 
+  {
+    path:'aigc',
+    canActivate:[authGuard],//路由守卫
+    children:[
+      {
+          path: 'agent-shige',
+          loadChildren: () => import('../modules/aigc/agent/agent-shige/agent-shige.module').then( m => m.AgentShigePageModule)
+      },
+      {
+          path: 'chat',
+          loadChildren: () => import('../modules/aigc/chat/chat.module').then( m => m.ChatPageModule)
+      },
+    ]
+  },
+
+  
 ];
 @NgModule({
   imports: [

+ 4 - 0
src/app/app.component.ts

@@ -1,5 +1,9 @@
 import { Component } from '@angular/core';
 
+import Parse  from 'parse';
+Parse.initialize("dev");
+Parse.serverURL="http://web2023.fmode.cn:9999/parse";
+
 @Component({
   selector: 'app-root',
   templateUrl: 'app.component.html',

+ 14 - 8
src/app/tab1/tab1.page.html

@@ -1,17 +1,23 @@
 <ion-header [translucent]="true">
   <ion-toolbar>
     <ion-title>
-      Tab 1
+      <img src="/assets/images/logo.png" class="title-image" />
+      MagicMirror
     </ion-title>
   </ion-toolbar>
 </ion-header>
 
 <ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
-    <ion-toolbar>
-      <ion-title size="large">Tab 1</ion-title>
-    </ion-toolbar>
-  </ion-header>
-
-  <app-explore-container name="Tab 1 page"></app-explore-container>
+  <ion-header collapse="condense"></ion-header>
+  
+  <div class="container">
+    <ion-card>
+      <ion-card-content>
+        <img id="face" src=" https://web2023.fmode.cn/jxufe/0225067/assets/images/home.png" class="captured-image" />
+      </ion-card-content>
+    </ion-card>
+    <ion-button class="capture-button" expand="block" routerLink="/study/results">
+      <ion-icon src=" https://web2023.fmode.cn/jxufe/0225067/assets/icon/camera.svg" class="button-icon"></ion-icon>
+    </ion-button>
+  </div>
 </ion-content>

+ 78 - 0
src/app/tab1/tab1.page.scss

@@ -0,0 +1,78 @@
+ion-content {
+    --background: transparent;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: relative;
+  }
+  
+  .container {
+    height: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: relative;
+  }
+  
+  ion-card {
+    width: 100%;
+    height: 100%;
+    margin: 0;
+    box-shadow: none;
+  }
+  
+  ion-card-content {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    padding: 0;
+  }
+  
+  .captured-image {
+    width: 100%;
+    height: 100%;
+    object-fit: cover;
+  }
+  
+  .title-image {
+    height: 24px; /* Adjust height as needed */
+    margin-right: 8px; /* Space between image and text */
+    vertical-align: middle;
+  }
+  
+  .capture-button {
+    position: absolute;
+    bottom: 16px; /* 根据需要调整距离底部的距离 */
+    left: 50%;
+    transform: translateX(-50%);
+    width: 128px; /* 按钮的宽度 */
+    height: 128px; /* 按钮的高度 */
+    border-radius: 50%; /* 将按钮设置为圆形 */
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 0;
+    background-color: transparent !important; /* 使用 !important 覆盖可能存在的默认样式 */
+    border: none !important; /* 去除边框并使用 !important 覆盖可能存在的默认样式 */
+  }
+  
+  .capture-button .button-native {
+    background-color: transparent !important; /* 针对Ionic的原生按钮样式,确保背景色是透明的 */
+  }
+  
+  
+  .button-icon {
+    width: 72px; /* Adjust icon size as needed */
+    height: 72px; /* Adjust icon size as needed */
+    color: white; /* Icon color */
+  }
+  
+  @media only screen and (device-width: 430px) and (device-height: 932px) and (-webkit-device-pixel-ratio: 3) {
+    .captured-image {
+      width: 2796px;
+      height: 1290px;
+      object-fit: cover;
+    }
+  }
+  

+ 378 - 5
src/app/tab1/tab1.page.ts

@@ -1,12 +1,385 @@
-import { Component } from '@angular/core';
-
+import { Component, ElementRef, OnInit, ViewChild, } from '@angular/core';
+import * as faceapi from '@vladmandic/face-api';
+import Parse from "parse"
+Parse.initialize("dev");
+Parse.serverURL="http://web2023.fmode.cn:9999/parse";
 @Component({
   selector: 'app-tab1',
   templateUrl: 'tab1.page.html',
-  styleUrls: ['tab1.page.scss']
+  styleUrls: ['tab1.page.scss'] 
 })
-export class Tab1Page {
+export class Tab1Page implements OnInit {
+
+  @ViewChild('video', { static: true }) videoElement: ElementRef|any;
+  @ViewChild('canvas', { static: true }) canvas: ElementRef|any;
+
+//  videoWidth = 0;
+//  videoHeight = 0;
+
+
+  // startCamera() {
+  //   if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+  //     navigator.mediaDevices.getUserMedia({ video: true })
+  //       .then(stream => {
+  //         this.videoElement.nativeElement.srcObject = stream;
+  //         this.videoElement.nativeElement.play();
+  //       })
+  //       .catch(err => console.error('Error accessing the camera: ', err));
+  //   }
+  // }
+
+  // capture() {
+  //   this.canvas.nativeElement.width = this.videoWidth;
+  //   this.canvas.nativeElement.height = this.videoHeight;
+  //   this.canvas.nativeElement.getContext('2d').drawImage(this.videoElement.nativeElement, 0, 0, this.videoWidth, this.videoHeight);
+  //   const imgSrc = this.canvas.nativeElement.toDataURL('image/png');
+  //   document.getElementById('face')?.setAttribute('src', imgSrc);
+
+  //   setTimeout(() => {
+  //       this.runFaceAnanlysis()//等待
+  //   }, 2000);
+  // }
+
+
+  constructor() { }
+
+ 
+  ngOnInit() {
+    this.runFaceAnanlysis();
+  }
+
+
+
+
+  onFileChange(event: any) {
+    const file = event.target.files[0];
+    if (file) {
+      const reader = new FileReader();
+      reader.onload = (e: any) => {
+        const img = new Image();
+        img.src = e.target.result;
+        img.onload = () => {
+          const canvasEl = this.canvas.nativeElement;
+          const context = canvasEl.getContext('2d');
+
+          canvasEl.width = img.width;
+          canvasEl.height = img.height;
+          context.drawImage(img, 0, 0);
+
+          setTimeout(() => {
+            this.runFaceAnanlysis();
+          }, 2000);
+        };
+        document.getElementById('face')?.setAttribute('src', e.target.result);
+      };
+      reader.readAsDataURL(file);
+    }
+  }
+
+
+  runFaceAnanlysis(){
+    console.log(faceapi);
+const calculateSanTing = (positions:any) => {
+  // 获取特定索引的特征点位置
+  const leftEye = positions[36]; // 左眼角
+  const rightEye = positions[42]; // 右眼角
+  const noseBase = positions[33]; // 鼻尖
+  const mouth = positions[48]; // 嘴巴中心
+
+  // 计算三庭的比例
+  const foreheadHeight = noseBase.x - leftEye.x; // 水平距离
+  const noseLength = rightEye.x - leftEye.x; // 水平距离
+  const chinHeight = mouth.x - noseBase.x; // 水平距离
+
+  const foreheadHeightRounded = parseFloat(foreheadHeight.toFixed(2));
+  const noseLengthRounded = parseFloat(noseLength.toFixed(2));
+  const chinHeightRounded = parseFloat(chinHeight.toFixed(2));
+
+  return {
+    foreheadHeight: foreheadHeightRounded,
+    noseLength: noseLengthRounded,
+    chinHeight: -chinHeightRounded,
+  };
+};
+
+
+const calculateWuYan = (positions:any) => {
+  // 获取特定索引的特征点位置
+  const leftEyebrow = positions[17]; // 左眉毛上角
+  const rightEyebrow = positions[22]; // 右眉毛上角
+
+  // 计算五眼的比例
+  const eyeSpan = Math.abs(leftEyebrow.x - rightEyebrow.x);
+  const fiveEyeSpan = eyeSpan * 5;
+
+  const fiveEyeSpanRounded = parseFloat(fiveEyeSpan.toFixed(2));
+
+  return fiveEyeSpanRounded;
+};
+
+const calculateFaceShape = (positions:any) => {
+    // 选择关键特征点
+    const jawOutline = positions.slice(0, 17); // 下巴轮廓
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+    const foreheadLeft = positions[21]; // 左额头
+    const foreheadRight = positions[22]; // 右额头
+  
+    // 计算宽度
+    const cheekWidth = Math.sqrt((cheekRight.x - cheekLeft.x) ** 2 + (cheekRight.y - cheekLeft.y) ** 2);
+    const foreheadWidth = Math.sqrt((foreheadRight.x - foreheadLeft.x) ** 2 + (foreheadRight.y - foreheadLeft.y) ** 2);
+    const jawWidth = Math.sqrt((jawOutline[8].x - jawOutline[6].x) ** 2 + (jawOutline[8].y - jawOutline[6].y) ** 2);
+  
+    // 计算长度
+    const faceLength = Math.sqrt((jawOutline[8].x - jawOutline[0].x) ** 2 + (jawOutline[8].y - jawOutline[0].y) ** 2);
+  
+    // 计算比例
+    const cheekForeheadRatio = cheekWidth / foreheadWidth;
+    const jawForeheadRatio = jawWidth / foreheadWidth;
+    const faceAspectRatio = faceLength / foreheadWidth;
+  
+    // 判断脸型
+    if (faceAspectRatio > 1.5) {
+      // 长脸
+      if (cheekForeheadRatio > 0.8 && cheekForeheadRatio < 1.2 && jawForeheadRatio < 0.8) {
+        return '瓜子脸';
+      } else {
+        return '长形脸';
+      }
+    } else if (faceAspectRatio < 1) {
+      // 宽脸
+      return '宽脸';
+    } else {
+      // 标准脸
+      if (cheekForeheadRatio > 0.8 && cheekForeheadRatio < 1.2 && jawForeheadRatio > 0.8) {
+        return '心形脸';
+      } else if (cheekForeheadRatio < 0.8 && jawForeheadRatio < 0.8) {
+        return '菱形脸';
+      } else {
+        return '标准脸';
+      }
+    }
+  };
+
+// 封装计算眉形的函数
+const calculateBrowShape = (positions:any) => {
+    // 选择关键特征点
+    const leftEyebrowLeft = positions[17]; // 左眉毛上角
+    const leftEyebrowRight = positions[22]; // 左眉毛下角
+    const rightEyebrowLeft = positions[25]; // 右眉毛上角
+    const rightEyebrowRight = positions[31]; // 右眉毛下角
+  
+    // 计算眉毛宽度
+    const leftBrowWidth = Math.abs(leftEyebrowLeft.x - leftEyebrowRight.x);
+    const rightBrowWidth = Math.abs(rightEyebrowLeft.x - rightEyebrowRight.x);
+    const browWidth = Math.max(leftBrowWidth, rightBrowWidth);
+  
+    // 计算眉毛长度
+    const leftBrowLength = Math.sqrt((leftEyebrowLeft.x - leftEyebrowRight.x) ** 2 + (leftEyebrowLeft.y - leftEyebrowRight.y) ** 2);
+    const rightBrowLength = Math.sqrt((rightEyebrowLeft.x - rightEyebrowRight.x) ** 2 + (rightEyebrowLeft.y - rightEyebrowRight.y) ** 2);
+    const browLength = Math.max(leftBrowLength, rightBrowLength);
+  
+    // 判断眉形
+    if (browWidth > 0.5 * browLength) {
+      return '浓眉';
+    } else if (browWidth < 0.3 * browLength) {
+      return '细眉';
+    } else {
+      return '标准眉';
+    }
+  
+    // 其他眉形判断条件可以添加在这里
+    // ...
+  };
+
+  // 封装计算唇形的函数
+const calculateLipShape = (positions:any) => {
+    // 选择关键特征点
+    const upperLipTop = positions[61]; // 上嘴唇顶部
+    const upperLipBottom = positions[62]; // 上嘴唇底部
+    const lowerLipTop = positions[63]; // 下嘴唇顶部
+    const lowerLipBottom = positions[64]; // 下嘴唇底部
+  
+    // 判断唇形
+    if (upperLipBottom.y - upperLipTop.y > 10 && lowerLipBottom.y - lowerLipTop.y > 10) {
+      return '厚唇';
+    } else if (upperLipBottom.y - upperLipTop.y < 5 && lowerLipBottom.y - lowerLipTop.y < 5) {
+      return '薄唇';
+    } else if (upperLipBottom.y - upperLipTop.y > 10 && lowerLipBottom.y - lowerLipTop.y < 5) {
+      return '微笑唇';
+    } else if (upperLipBottom.y - upperLipTop.y < 5 && lowerLipBottom.y - lowerLipTop.y > 10) {
+      return '嘟嘟唇';
+    } else {
+      return '标准唇型';
+    }
+  };
+  // 封装计算眼睛形状的函数
+const calculateEyeShape = (positions:any) => {
+    // 选择关键特征点
+    const leftEyeInner = positions[33]; // 左眼内侧
+    const leftEyeOuter = positions[39]; // 左眼外侧
+    const rightEyeInner = positions[45]; // 右眼内侧
+    const rightEyeOuter = positions[51]; // 右眼外侧
+  
+    // 计算眼睛长度
+    const leftEyeLength = Math.sqrt((leftEyeInner.x - leftEyeOuter.x) ** 2 + (leftEyeInner.y - leftEyeOuter.y) ** 2);
+    const rightEyeLength = Math.sqrt((rightEyeInner.x - rightEyeOuter.x) ** 2 + (rightEyeInner.y - rightEyeOuter.y) ** 2);
+    const eyeLength = Math.max(leftEyeLength, rightEyeLength);
+  
+    // 判断眼睛形状
+    if (eyeLength > 10 && leftEyeLength > rightEyeLength) {
+      return '杏仁眼';
+    } else if (eyeLength > 10 && leftEyeLength < rightEyeLength) {
+      return '圆形眼';
+    } else if (leftEyeInner.y > leftEyeOuter.y) {
+      return '单眼皮';
+    } else {
+      return '双眼皮';
+    }
+  };
+  
+  // 封装计算鼻形的函数
+  const calculateNoseShape = (positions:any) => {
+    // 选择关键特征点
+    const noseBase = positions[33]; // 鼻尖
+    const noseTip = positions[34]; // 鼻尖
+    const noseSide = positions[35]; // 鼻翼
+  
+    // 判断鼻形
+    if (noseBase.y > noseTip.y && noseSide.x > noseTip.x) {
+      return '鹰钩鼻';
+    } else if (noseBase.y > noseTip.y && noseSide.x < noseTip.x) {
+      return '蒜头鼻';
+    } else if (noseBase.y < noseTip.y) {
+      return '直鼻';
+    } else {
+      return '塌鼻';
+    }
+  };
+  
+  // 封装计算颧骨的函数
+  const calculateCheekBones = (positions:any) => {
+    // 选择关键特征点
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+  
+    // 判断颧骨
+    if (cheekLeft.y > cheekRight.y) {
+      return '颧骨高';
+    } else {
+      return '颧骨平';
+    }
+  };
+  
+  // 封装计算下巴的函数
+  const calculateChin = (positions:any) => {
+    // 选择关键特征点
+    const chin = positions[60]; // 下巴中心
+  
+    // 判断下巴
+    if (chin.y < chin.x) {
+      return '长下巴';
+    } else if (chin.y > chin.x) {
+      return '短下巴';
+    } else {
+      return '尖下巴';
+    }
+  };
+  
+  // 封装计算额头的函数
+  const calculateForehead = (positions:any) => {
+    // 选择关键特征点
+    const foreheadLeft = positions[21]; // 左额头
+    const foreheadRight = positions[22]; // 右额头
+  
+    // 判断额头
+    if (foreheadLeft.x > foreheadRight.x) {
+      return '宽额头';
+    } else {
+      return '窄额头';
+    }
+  };
+  
+
+  const calculateCheeksShape = (positions:any) => {
+    // 选择关键特征点
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+  
+    // 判断脸颊形状
+    if (cheekLeft.y > cheekRight.y) {
+      return '丰满脸颊';
+    } else {
+      return '瘦削脸颊';
+    }
+  };
+
+  
+
+  
+const run = async () => {
+  // 加载模型
+  await Promise.all([
+    faceapi.nets.ssdMobilenetv1.loadFromUri('/assets/models'),
+    faceapi.nets.faceLandmark68Net.loadFromUri('/assets/models'),
+    faceapi.nets.faceRecognitionNet.loadFromUri('/assets/models'),
+    faceapi.nets.ageGenderNet.loadFromUri('/assets/models'),
+  ]);
+
+  const face1:any = document.getElementById('face');
+
+  let faceAIData = await faceapi
+    .detectAllFaces(face1)
+    .withFaceLandmarks()
+    .withFaceDescriptors()
+    .withAgeAndGender();
+
+  console.log(faceAIData);
+
+  // 获取第一个检测到的人脸的特征点
+  const landmarks:any = faceAIData[0].landmarks;
+  const positions:any = landmarks._positions;
+
+  // 计算三庭和五眼的比例
+  const sanTing = calculateSanTing(positions);//计算三庭
+  const wuYan = calculateWuYan(positions);//计算五眼
+  // 计算脸型
+  const faceShape = calculateFaceShape(positions);
+  // 计算眉形
+  const browShape = calculateBrowShape(positions);
+  // 计算唇形
+  const lipShape = calculateLipShape(positions);
+  // 计算眼睛形状
+  const eyeShape = calculateEyeShape(positions);
+  // 计算鼻形
+  const noseShape = calculateNoseShape(positions);
+  // 计算颧骨
+  const cheekBones = calculateCheekBones(positions);
+  // 计算下巴
+  const chinShape = calculateChin(positions);
+  // 计算额头
+  const foreheadShape = calculateForehead(positions);
+  // 计算脸颊
+  const cheeksShape = calculateCheeksShape(positions);
+
+ console.log(`三庭比例: ${sanTing.foreheadHeight},${sanTing.noseLength}, ${sanTing.chinHeight}`);
+ console.log(`五眼比例: ${wuYan}`);
+ console.log(`脸型: ${faceShape}`);
+ console.log(`眉形: ${browShape}`);
+ console.log(`唇形: ${lipShape}`);
+ console.log(`眼睛形状: ${eyeShape}`);
+ console.log(`鼻形: ${noseShape}`);
+ console.log(`颧骨: ${cheekBones}`);
+ console.log(`下巴: ${chinShape}`);
+ console.log(`额头: ${foreheadShape}`);
+ console.log(`脸颊: ${cheeksShape}`);
+
+
+};
+
+
 
-  constructor() {}
+//run();
+  }
 
 }

+ 7 - 0
src/app/tabs/tabs-routing.module.ts

@@ -1,6 +1,7 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
 import { TabsPage } from './tabs.page';
+import { authGuard } from 'src/modules/user/auth.guard';
 
 const routes: Routes = [
   {
@@ -13,12 +14,18 @@ const routes: Routes = [
       },
       {
         path: 'tab2',
+        canActivate:[authGuard],//路由守卫
         loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule)
       },
       {
         path: 'tab3',
         loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule)
       },
+      {
+        path: 'mine',
+        loadChildren: () => import('../../modules/user/mine/mine.module').then(m => m.MinePageModule)
+      },
+
       {
         path: '',
         redirectTo: '/tabs/tab1',

+ 5 - 1
src/app/tabs/tabs.page.html

@@ -13,7 +13,11 @@
 
     <ion-tab-button tab="tab3" href="/tabs/tab3">
       <ion-icon aria-hidden="true" name="square"></ion-icon>
-      <ion-label>Tab 3</ion-label>
+      <ion-label>历史</ion-label>
+    </ion-tab-button>
+    <ion-tab-button tab="mine" href="/tabs/mine">
+      <ion-icon aria-hidden="true" name="person"></ion-icon>
+      <ion-label>我的</ion-label>
     </ion-tab-button>
   </ion-tab-bar>
 

Plik diff jest za duży
+ 2 - 0
src/assets/icon/camera.svg


BIN
src/assets/images/666.jpg


BIN
src/assets/images/BackGround.png


BIN
src/assets/images/girlsSmile1.png


BIN
src/assets/images/home.png


+ 14 - 0
src/assets/index.html

@@ -0,0 +1,14 @@
+<style>
+    img{
+        max-width: 45%
+    }
+    canvas{
+        position: absolute;
+    }
+</style>
+
+<canvas id="canvas"></canvas>
+
+<!-- <img id="face" src="./images/oldMan2.png" />
+<script src="./face-api.min.js"></script>
+<script src="./scripts.js"></script> -->

BIN
src/assets/models/age_gender_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/age_gender_model-weights_manifest.json


BIN
src/assets/models/face_expression_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/face_expression_model-weights_manifest.json


BIN
src/assets/models/face_landmark_68_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/face_landmark_68_model-weights_manifest.json


BIN
src/assets/models/face_landmark_68_tiny_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/face_landmark_68_tiny_model-weights_manifest.json


BIN
src/assets/models/face_recognition_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/face_recognition_model-shard2


Plik diff jest za duży
+ 0 - 0
src/assets/models/face_recognition_model-weights_manifest.json


BIN
src/assets/models/mtcnn_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/mtcnn_model-weights_manifest.json


BIN
src/assets/models/ssd_mobilenetv1_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/ssd_mobilenetv1_model-shard2


Plik diff jest za duży
+ 0 - 0
src/assets/models/ssd_mobilenetv1_model-weights_manifest.json


BIN
src/assets/models/tiny_face_detector_model-shard1


Plik diff jest za duży
+ 0 - 0
src/assets/models/tiny_face_detector_model-weights_manifest.json


Plik diff jest za duży
+ 0 - 0
src/assets/script/face-api.min.js


+ 298 - 0
src/assets/script/scripts.js

@@ -0,0 +1,298 @@
+console.log(faceapi);
+const calculateSanTing = (positions) => {
+  // 获取特定索引的特征点位置
+  const leftEye = positions[36]; // 左眼角
+  const rightEye = positions[42]; // 右眼角
+  const noseBase = positions[33]; // 鼻尖
+  const mouth = positions[48]; // 嘴巴中心
+
+  // 计算三庭的比例
+  const foreheadHeight = noseBase.x - leftEye.x; // 水平距离
+  const noseLength = rightEye.x - leftEye.x; // 水平距离
+  const chinHeight = mouth.x - noseBase.x; // 水平距离
+
+  const foreheadHeightRounded = parseFloat(foreheadHeight.toFixed(2));
+  const noseLengthRounded = parseFloat(noseLength.toFixed(2));
+  const chinHeightRounded = parseFloat(chinHeight.toFixed(2));
+
+  return {
+    foreheadHeight: foreheadHeightRounded,
+    noseLength: noseLengthRounded,
+    chinHeight: -chinHeightRounded,
+  };
+};
+
+
+const calculateWuYan = (positions) => {
+  // 获取特定索引的特征点位置
+  const leftEyebrow = positions[17]; // 左眉毛上角
+  const rightEyebrow = positions[22]; // 右眉毛上角
+
+  // 计算五眼的比例
+  const eyeSpan = Math.abs(leftEyebrow.x - rightEyebrow.x);
+  const fiveEyeSpan = eyeSpan * 5;
+
+  const fiveEyeSpanRounded = parseFloat(fiveEyeSpan.toFixed(2));
+
+  return fiveEyeSpanRounded;
+};
+
+const calculateFaceShape = (positions) => {
+    // 选择关键特征点
+    const jawOutline = positions.slice(0, 17); // 下巴轮廓
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+    const foreheadLeft = positions[21]; // 左额头
+    const foreheadRight = positions[22]; // 右额头
+  
+    // 计算宽度
+    const cheekWidth = Math.sqrt((cheekRight.x - cheekLeft.x) ** 2 + (cheekRight.y - cheekLeft.y) ** 2);
+    const foreheadWidth = Math.sqrt((foreheadRight.x - foreheadLeft.x) ** 2 + (foreheadRight.y - foreheadLeft.y) ** 2);
+    const jawWidth = Math.sqrt((jawOutline[8].x - jawOutline[6].x) ** 2 + (jawOutline[8].y - jawOutline[6].y) ** 2);
+  
+    // 计算长度
+    const faceLength = Math.sqrt((jawOutline[8].x - jawOutline[0].x) ** 2 + (jawOutline[8].y - jawOutline[0].y) ** 2);
+  
+    // 计算比例
+    const cheekForeheadRatio = cheekWidth / foreheadWidth;
+    const jawForeheadRatio = jawWidth / foreheadWidth;
+    const faceAspectRatio = faceLength / foreheadWidth;
+  
+    // 判断脸型
+    if (faceAspectRatio > 1.5) {
+      // 长脸
+      if (cheekForeheadRatio > 0.8 && cheekForeheadRatio < 1.2 && jawForeheadRatio < 0.8) {
+        return '瓜子脸';
+      } else {
+        return '长形脸';
+      }
+    } else if (faceAspectRatio < 1) {
+      // 宽脸
+      return '宽脸';
+    } else {
+      // 标准脸
+      if (cheekForeheadRatio > 0.8 && cheekForeheadRatio < 1.2 && jawForeheadRatio > 0.8) {
+        return '心形脸';
+      } else if (cheekForeheadRatio < 0.8 && jawForeheadRatio < 0.8) {
+        return '菱形脸';
+      } else {
+        return '标准脸';
+      }
+    }
+  };
+
+// 封装计算眉形的函数
+const calculateBrowShape = (positions) => {
+    // 选择关键特征点
+    const leftEyebrowLeft = positions[17]; // 左眉毛上角
+    const leftEyebrowRight = positions[22]; // 左眉毛下角
+    const rightEyebrowLeft = positions[25]; // 右眉毛上角
+    const rightEyebrowRight = positions[31]; // 右眉毛下角
+  
+    // 计算眉毛宽度
+    const leftBrowWidth = Math.abs(leftEyebrowLeft.x - leftEyebrowRight.x);
+    const rightBrowWidth = Math.abs(rightEyebrowLeft.x - rightEyebrowRight.x);
+    const browWidth = Math.max(leftBrowWidth, rightBrowWidth);
+  
+    // 计算眉毛长度
+    const leftBrowLength = Math.sqrt((leftEyebrowLeft.x - leftEyebrowRight.x) ** 2 + (leftEyebrowLeft.y - leftEyebrowRight.y) ** 2);
+    const rightBrowLength = Math.sqrt((rightEyebrowLeft.x - rightEyebrowRight.x) ** 2 + (rightEyebrowLeft.y - rightEyebrowRight.y) ** 2);
+    const browLength = Math.max(leftBrowLength, rightBrowLength);
+  
+    // 判断眉形
+    if (browWidth > 0.5 * browLength) {
+      return '浓眉';
+    } else if (browWidth < 0.3 * browLength) {
+      return '细眉';
+    } else {
+      return '标准眉';
+    }
+  
+    // 其他眉形判断条件可以添加在这里
+    // ...
+  };
+
+  // 封装计算唇形的函数
+const calculateLipShape = (positions) => {
+    // 选择关键特征点
+    const upperLipTop = positions[61]; // 上嘴唇顶部
+    const upperLipBottom = positions[62]; // 上嘴唇底部
+    const lowerLipTop = positions[63]; // 下嘴唇顶部
+    const lowerLipBottom = positions[64]; // 下嘴唇底部
+  
+    // 判断唇形
+    if (upperLipBottom.y - upperLipTop.y > 10 && lowerLipBottom.y - lowerLipTop.y > 10) {
+      return '厚唇';
+    } else if (upperLipBottom.y - upperLipTop.y < 5 && lowerLipBottom.y - lowerLipTop.y < 5) {
+      return '薄唇';
+    } else if (upperLipBottom.y - upperLipTop.y > 10 && lowerLipBottom.y - lowerLipTop.y < 5) {
+      return '微笑唇';
+    } else if (upperLipBottom.y - upperLipTop.y < 5 && lowerLipBottom.y - lowerLipTop.y > 10) {
+      return '嘟嘟唇';
+    } else {
+      return '标准唇型';
+    }
+  };
+  // 封装计算眼睛形状的函数
+const calculateEyeShape = (positions) => {
+    // 选择关键特征点
+    const leftEyeInner = positions[33]; // 左眼内侧
+    const leftEyeOuter = positions[39]; // 左眼外侧
+    const rightEyeInner = positions[45]; // 右眼内侧
+    const rightEyeOuter = positions[51]; // 右眼外侧
+  
+    // 计算眼睛长度
+    const leftEyeLength = Math.sqrt((leftEyeInner.x - leftEyeOuter.x) ** 2 + (leftEyeInner.y - leftEyeOuter.y) ** 2);
+    const rightEyeLength = Math.sqrt((rightEyeInner.x - rightEyeOuter.x) ** 2 + (rightEyeInner.y - rightEyeOuter.y) ** 2);
+    const eyeLength = Math.max(leftEyeLength, rightEyeLength);
+  
+    // 判断眼睛形状
+    if (eyeLength > 10 && leftEyeLength > rightEyeLength) {
+      return '杏仁眼';
+    } else if (eyeLength > 10 && leftEyeLength < rightEyeLength) {
+      return '圆形眼';
+    } else if (leftEyeInner.y > leftEyeOuter.y) {
+      return '单眼皮';
+    } else {
+      return '双眼皮';
+    }
+  };
+  
+  // 封装计算鼻形的函数
+  const calculateNoseShape = (positions) => {
+    // 选择关键特征点
+    const noseBase = positions[33]; // 鼻尖
+    const noseTip = positions[34]; // 鼻尖
+    const noseSide = positions[35]; // 鼻翼
+  
+    // 判断鼻形
+    if (noseBase.y > noseTip.y && noseSide.x > noseTip.x) {
+      return '鹰钩鼻';
+    } else if (noseBase.y > noseTip.y && noseSide.x < noseTip.x) {
+      return '蒜头鼻';
+    } else if (noseBase.y < noseTip.y) {
+      return '直鼻';
+    } else {
+      return '塌鼻';
+    }
+  };
+  
+  // 封装计算颧骨的函数
+  const calculateCheekBones = (positions) => {
+    // 选择关键特征点
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+  
+    // 判断颧骨
+    if (cheekLeft.y > cheekRight.y) {
+      return '颧骨高';
+    } else {
+      return '颧骨平';
+    }
+  };
+  
+  // 封装计算下巴的函数
+  const calculateChin = (positions) => {
+    // 选择关键特征点
+    const chin = positions[60]; // 下巴中心
+  
+    // 判断下巴
+    if (chin.y < chin.x) {
+      return '长下巴';
+    } else if (chin.y > chin.x) {
+      return '短下巴';
+    } else {
+      return '尖下巴';
+    }
+  };
+  
+  // 封装计算额头的函数
+  const calculateForehead = (positions) => {
+    // 选择关键特征点
+    const foreheadLeft = positions[21]; // 左额头
+    const foreheadRight = positions[22]; // 右额头
+  
+    // 判断额头
+    if (foreheadLeft.x > foreheadRight.x) {
+      return '宽额头';
+    } else {
+      return '窄额头';
+    }
+  };
+  
+
+  const calculateCheeksShape = (positions) => {
+    // 选择关键特征点
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+  
+    // 判断脸颊形状
+    if (cheekLeft.y > cheekRight.y) {
+      return '丰满脸颊';
+    } else {
+      return '瘦削脸颊';
+    }
+  };
+
+  
+
+  
+const run = async () => {
+  // 加载模型
+  await Promise.all([
+    faceapi.nets.ssdMobilenetv1.loadFromUri('./models'),
+    faceapi.nets.faceLandmark68Net.loadFromUri('./models'),
+    faceapi.nets.faceRecognitionNet.loadFromUri('./models'),
+    faceapi.nets.ageGenderNet.loadFromUri('./models'),
+  ]);
+
+  const face1 = document.getElementById('face');
+
+  let faceAIData = await faceapi
+    .detectAllFaces(face1)
+    .withFaceLandmarks()
+    .withFaceDescriptors()
+    .withAgeAndGender();
+
+  console.log(faceAIData);
+
+  // 获取第一个检测到的人脸的特征点
+  const landmarks = faceAIData[0].landmarks;
+  const positions = landmarks._positions;
+
+  // 计算三庭和五眼的比例
+  const sanTing = calculateSanTing(positions);//计算三庭
+  const wuYan = calculateWuYan(positions);//计算五眼
+  // 计算脸型
+  const faceShape = calculateFaceShape(positions);
+  // 计算眉形
+  const browShape = calculateBrowShape(positions);
+  // 计算唇形
+  const lipShape = calculateLipShape(positions);
+  // 计算眼睛形状
+  const eyeShape = calculateEyeShape(positions);
+  // 计算鼻形
+  const noseShape = calculateNoseShape(positions);
+  // 计算颧骨
+  const cheekBones = calculateCheekBones(positions);
+  // 计算下巴
+  const chinShape = calculateChin(positions);
+  // 计算额头
+  const foreheadShape = calculateForehead(positions);
+  // 计算脸颊
+  const cheeksShape = calculateCheeksShape(positions);
+
+ console.log(`三庭比例: ${sanTing.foreheadHeight},${sanTing.noseLength}, ${sanTing.chinHeight}`);
+ console.log(`五眼比例: ${wuYan}`);
+ console.log(`脸型: ${faceShape}`);
+ console.log(`眉形: ${browShape}`);
+ console.log(`唇形: ${lipShape}`);
+ console.log(`眼睛形状: ${eyeShape}`);
+ console.log(`鼻形: ${noseShape}`);
+ console.log(`颧骨: ${cheekBones}`);
+ console.log(`下巴: ${chinShape}`);
+ console.log(`额头: ${foreheadShape}`);
+ console.log(`脸颊: ${cheeksShape}`);
+};
+
+run();

+ 2 - 0
src/face-api-js-starter-main/.gitignore

@@ -0,0 +1,2 @@
+node_modules
+package-lock.json

+ 3 - 0
src/face-api-js-starter-main/.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "liveServer.settings.port": 5501
+}

+ 16 - 0
src/face-api-js-starter-main/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "face-api",
+  "version": "1.0.0",
+  "description": "",
+  "main": "server.js",
+  "dependencies": {
+    "express": "^4.18.2"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1",
+    "start": "node server.js"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC"
+}

Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/face-api.min.js


BIN
src/face-api-js-starter-main/public/images/girlsSmile1.png


BIN
src/face-api-js-starter-main/public/images/girlsSmile2.png


BIN
src/face-api-js-starter-main/public/images/girlsSmile3.png


BIN
src/face-api-js-starter-main/public/images/girlsSmile4.png


BIN
src/face-api-js-starter-main/public/images/oldMan1.png


BIN
src/face-api-js-starter-main/public/images/oldMan2.png


BIN
src/face-api-js-starter-main/public/images/oldManSerious1.png


BIN
src/face-api-js-starter-main/public/images/oldManSerious2.png


BIN
src/face-api-js-starter-main/public/images/oldManSmiling.png


BIN
src/face-api-js-starter-main/public/images/oldWoman.png


BIN
src/face-api-js-starter-main/public/images/waterfall1.png


BIN
src/face-api-js-starter-main/public/images/waterfall2.png


BIN
src/face-api-js-starter-main/public/images/youngman1.png


BIN
src/face-api-js-starter-main/public/images/youngman2.png


+ 14 - 0
src/face-api-js-starter-main/public/index.html

@@ -0,0 +1,14 @@
+<style>
+    img{
+        max-width: 45%
+    }
+    canvas{
+        position: absolute;
+    }
+</style>
+
+<canvas id="canvas"></canvas>
+
+<img id="face" src="./images/oldMan2.png" />
+<script src="./face-api.min.js"></script>
+<script src="./scripts.js"></script>

BIN
src/face-api-js-starter-main/public/models/age_gender_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/age_gender_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/face_expression_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/face_expression_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/face_landmark_68_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/face_landmark_68_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/face_landmark_68_tiny_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/face_landmark_68_tiny_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/face_recognition_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/face_recognition_model-shard2


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/face_recognition_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/mtcnn_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/mtcnn_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/ssd_mobilenetv1_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/ssd_mobilenetv1_model-shard2


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/ssd_mobilenetv1_model-weights_manifest.json


BIN
src/face-api-js-starter-main/public/models/tiny_face_detector_model-shard1


Plik diff jest za duży
+ 0 - 0
src/face-api-js-starter-main/public/models/tiny_face_detector_model-weights_manifest.json


+ 298 - 0
src/face-api-js-starter-main/public/scripts.js

@@ -0,0 +1,298 @@
+console.log(faceapi);
+const calculateSanTing = (positions) => {
+  // 获取特定索引的特征点位置
+  const leftEye = positions[36]; // 左眼角
+  const rightEye = positions[42]; // 右眼角
+  const noseBase = positions[33]; // 鼻尖
+  const mouth = positions[48]; // 嘴巴中心
+
+  // 计算三庭的比例
+  const foreheadHeight = noseBase.x - leftEye.x; // 水平距离
+  const noseLength = rightEye.x - leftEye.x; // 水平距离
+  const chinHeight = mouth.x - noseBase.x; // 水平距离
+
+  const foreheadHeightRounded = parseFloat(foreheadHeight.toFixed(2));
+  const noseLengthRounded = parseFloat(noseLength.toFixed(2));
+  const chinHeightRounded = parseFloat(chinHeight.toFixed(2));
+
+  return {
+    foreheadHeight: foreheadHeightRounded,
+    noseLength: noseLengthRounded,
+    chinHeight: -chinHeightRounded,
+  };
+};
+
+
+const calculateWuYan = (positions) => {
+  // 获取特定索引的特征点位置
+  const leftEyebrow = positions[17]; // 左眉毛上角
+  const rightEyebrow = positions[22]; // 右眉毛上角
+
+  // 计算五眼的比例
+  const eyeSpan = Math.abs(leftEyebrow.x - rightEyebrow.x);
+  const fiveEyeSpan = eyeSpan * 5;
+
+  const fiveEyeSpanRounded = parseFloat(fiveEyeSpan.toFixed(2));
+
+  return fiveEyeSpanRounded;
+};
+
+const calculateFaceShape = (positions) => {
+    // 选择关键特征点
+    const jawOutline = positions.slice(0, 17); // 下巴轮廓
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+    const foreheadLeft = positions[21]; // 左额头
+    const foreheadRight = positions[22]; // 右额头
+  
+    // 计算宽度
+    const cheekWidth = Math.sqrt((cheekRight.x - cheekLeft.x) ** 2 + (cheekRight.y - cheekLeft.y) ** 2);
+    const foreheadWidth = Math.sqrt((foreheadRight.x - foreheadLeft.x) ** 2 + (foreheadRight.y - foreheadLeft.y) ** 2);
+    const jawWidth = Math.sqrt((jawOutline[8].x - jawOutline[6].x) ** 2 + (jawOutline[8].y - jawOutline[6].y) ** 2);
+  
+    // 计算长度
+    const faceLength = Math.sqrt((jawOutline[8].x - jawOutline[0].x) ** 2 + (jawOutline[8].y - jawOutline[0].y) ** 2);
+  
+    // 计算比例
+    const cheekForeheadRatio = cheekWidth / foreheadWidth;
+    const jawForeheadRatio = jawWidth / foreheadWidth;
+    const faceAspectRatio = faceLength / foreheadWidth;
+  
+    // 判断脸型
+    if (faceAspectRatio > 1.5) {
+      // 长脸
+      if (cheekForeheadRatio > 0.8 && cheekForeheadRatio < 1.2 && jawForeheadRatio < 0.8) {
+        return '瓜子脸';
+      } else {
+        return '长形脸';
+      }
+    } else if (faceAspectRatio < 1) {
+      // 宽脸
+      return '宽脸';
+    } else {
+      // 标准脸
+      if (cheekForeheadRatio > 0.8 && cheekForeheadRatio < 1.2 && jawForeheadRatio > 0.8) {
+        return '心形脸';
+      } else if (cheekForeheadRatio < 0.8 && jawForeheadRatio < 0.8) {
+        return '菱形脸';
+      } else {
+        return '标准脸';
+      }
+    }
+  };
+
+// 封装计算眉形的函数
+const calculateBrowShape = (positions) => {
+    // 选择关键特征点
+    const leftEyebrowLeft = positions[17]; // 左眉毛上角
+    const leftEyebrowRight = positions[22]; // 左眉毛下角
+    const rightEyebrowLeft = positions[25]; // 右眉毛上角
+    const rightEyebrowRight = positions[31]; // 右眉毛下角
+  
+    // 计算眉毛宽度
+    const leftBrowWidth = Math.abs(leftEyebrowLeft.x - leftEyebrowRight.x);
+    const rightBrowWidth = Math.abs(rightEyebrowLeft.x - rightEyebrowRight.x);
+    const browWidth = Math.max(leftBrowWidth, rightBrowWidth);
+  
+    // 计算眉毛长度
+    const leftBrowLength = Math.sqrt((leftEyebrowLeft.x - leftEyebrowRight.x) ** 2 + (leftEyebrowLeft.y - leftEyebrowRight.y) ** 2);
+    const rightBrowLength = Math.sqrt((rightEyebrowLeft.x - rightEyebrowRight.x) ** 2 + (rightEyebrowLeft.y - rightEyebrowRight.y) ** 2);
+    const browLength = Math.max(leftBrowLength, rightBrowLength);
+  
+    // 判断眉形
+    if (browWidth > 0.5 * browLength) {
+      return '浓眉';
+    } else if (browWidth < 0.3 * browLength) {
+      return '细眉';
+    } else {
+      return '标准眉';
+    }
+  
+    // 其他眉形判断条件可以添加在这里
+    // ...
+  };
+
+  // 封装计算唇形的函数
+const calculateLipShape = (positions) => {
+    // 选择关键特征点
+    const upperLipTop = positions[61]; // 上嘴唇顶部
+    const upperLipBottom = positions[62]; // 上嘴唇底部
+    const lowerLipTop = positions[63]; // 下嘴唇顶部
+    const lowerLipBottom = positions[64]; // 下嘴唇底部
+  
+    // 判断唇形
+    if (upperLipBottom.y - upperLipTop.y > 10 && lowerLipBottom.y - lowerLipTop.y > 10) {
+      return '厚唇';
+    } else if (upperLipBottom.y - upperLipTop.y < 5 && lowerLipBottom.y - lowerLipTop.y < 5) {
+      return '薄唇';
+    } else if (upperLipBottom.y - upperLipTop.y > 10 && lowerLipBottom.y - lowerLipTop.y < 5) {
+      return '微笑唇';
+    } else if (upperLipBottom.y - upperLipTop.y < 5 && lowerLipBottom.y - lowerLipTop.y > 10) {
+      return '嘟嘟唇';
+    } else {
+      return '标准唇型';
+    }
+  };
+  // 封装计算眼睛形状的函数
+const calculateEyeShape = (positions) => {
+    // 选择关键特征点
+    const leftEyeInner = positions[33]; // 左眼内侧
+    const leftEyeOuter = positions[39]; // 左眼外侧
+    const rightEyeInner = positions[45]; // 右眼内侧
+    const rightEyeOuter = positions[51]; // 右眼外侧
+  
+    // 计算眼睛长度
+    const leftEyeLength = Math.sqrt((leftEyeInner.x - leftEyeOuter.x) ** 2 + (leftEyeInner.y - leftEyeOuter.y) ** 2);
+    const rightEyeLength = Math.sqrt((rightEyeInner.x - rightEyeOuter.x) ** 2 + (rightEyeInner.y - rightEyeOuter.y) ** 2);
+    const eyeLength = Math.max(leftEyeLength, rightEyeLength);
+  
+    // 判断眼睛形状
+    if (eyeLength > 10 && leftEyeLength > rightEyeLength) {
+      return '杏仁眼';
+    } else if (eyeLength > 10 && leftEyeLength < rightEyeLength) {
+      return '圆形眼';
+    } else if (leftEyeInner.y > leftEyeOuter.y) {
+      return '单眼皮';
+    } else {
+      return '双眼皮';
+    }
+  };
+  
+  // 封装计算鼻形的函数
+  const calculateNoseShape = (positions) => {
+    // 选择关键特征点
+    const noseBase = positions[33]; // 鼻尖
+    const noseTip = positions[34]; // 鼻尖
+    const noseSide = positions[35]; // 鼻翼
+  
+    // 判断鼻形
+    if (noseBase.y > noseTip.y && noseSide.x > noseTip.x) {
+      return '鹰钩鼻';
+    } else if (noseBase.y > noseTip.y && noseSide.x < noseTip.x) {
+      return '蒜头鼻';
+    } else if (noseBase.y < noseTip.y) {
+      return '直鼻';
+    } else {
+      return '塌鼻';
+    }
+  };
+  
+  // 封装计算颧骨的函数
+  const calculateCheekBones = (positions) => {
+    // 选择关键特征点
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+  
+    // 判断颧骨
+    if (cheekLeft.y > cheekRight.y) {
+      return '颧骨高';
+    } else {
+      return '颧骨平';
+    }
+  };
+  
+  // 封装计算下巴的函数
+  const calculateChin = (positions) => {
+    // 选择关键特征点
+    const chin = positions[60]; // 下巴中心
+  
+    // 判断下巴
+    if (chin.y < chin.x) {
+      return '长下巴';
+    } else if (chin.y > chin.x) {
+      return '短下巴';
+    } else {
+      return '尖下巴';
+    }
+  };
+  
+  // 封装计算额头的函数
+  const calculateForehead = (positions) => {
+    // 选择关键特征点
+    const foreheadLeft = positions[21]; // 左额头
+    const foreheadRight = positions[22]; // 右额头
+  
+    // 判断额头
+    if (foreheadLeft.x > foreheadRight.x) {
+      return '宽额头';
+    } else {
+      return '窄额头';
+    }
+  };
+  
+
+  const calculateCheeksShape = (positions) => {
+    // 选择关键特征点
+    const cheekLeft = positions[29]; // 左颧骨
+    const cheekRight = positions[30]; // 右颧骨
+  
+    // 判断脸颊形状
+    if (cheekLeft.y > cheekRight.y) {
+      return '丰满脸颊';
+    } else {
+      return '瘦削脸颊';
+    }
+  };
+
+  
+
+  
+const run = async () => {
+  // 加载模型
+  await Promise.all([
+    faceapi.nets.ssdMobilenetv1.loadFromUri('./models'),
+    faceapi.nets.faceLandmark68Net.loadFromUri('./models'),
+    faceapi.nets.faceRecognitionNet.loadFromUri('./models'),
+    faceapi.nets.ageGenderNet.loadFromUri('./models'),
+  ]);
+
+  const face1 = document.getElementById('face');
+
+  let faceAIData = await faceapi
+    .detectAllFaces(face1)
+    .withFaceLandmarks()
+    .withFaceDescriptors()
+    .withAgeAndGender();
+
+  console.log(faceAIData);
+
+  // 获取第一个检测到的人脸的特征点
+  const landmarks = faceAIData[0].landmarks;
+  const positions = landmarks._positions;
+
+  // 计算三庭和五眼的比例
+  const sanTing = calculateSanTing(positions);//计算三庭
+  const wuYan = calculateWuYan(positions);//计算五眼
+  // 计算脸型
+  const faceShape = calculateFaceShape(positions);
+  // 计算眉形
+  const browShape = calculateBrowShape(positions);
+  // 计算唇形
+  const lipShape = calculateLipShape(positions);
+  // 计算眼睛形状
+  const eyeShape = calculateEyeShape(positions);
+  // 计算鼻形
+  const noseShape = calculateNoseShape(positions);
+  // 计算颧骨
+  const cheekBones = calculateCheekBones(positions);
+  // 计算下巴
+  const chinShape = calculateChin(positions);
+  // 计算额头
+  const foreheadShape = calculateForehead(positions);
+  // 计算脸颊
+  const cheeksShape = calculateCheeksShape(positions);
+
+ console.log(`三庭比例: ${sanTing.foreheadHeight},${sanTing.noseLength}, ${sanTing.chinHeight}`);
+ console.log(`五眼比例: ${wuYan}`);
+ console.log(`脸型: ${faceShape}`);
+ console.log(`眉形: ${browShape}`);
+ console.log(`唇形: ${lipShape}`);
+ console.log(`眼睛形状: ${eyeShape}`);
+ console.log(`鼻形: ${noseShape}`);
+ console.log(`颧骨: ${cheekBones}`);
+ console.log(`下巴: ${chinShape}`);
+ console.log(`额头: ${foreheadShape}`);
+ console.log(`脸颊: ${cheeksShape}`);
+};
+
+run();

+ 30 - 0
src/face-api-js-starter-main/readme.md

@@ -0,0 +1,30 @@
+# Facial Recognition with JavaScript using face-api.js
+### To start up the app:
+1. run npm install in the root directory
+2. run node on server.js
+3. go to http://localhost:5000
+### images in public were generated with fooocus. None are known to have any actual people in them
+
+### [Face API Github](https://github.com/justadudewhohacks/face-api.js)
+
+### Loading 4 primary models
+``` javascript
+    await Promise.all([
+        faceapi.nets.ssdMobilenetv1.loadFromUri('./models'),
+        faceapi.nets.faceLandmark68Net.loadFromUri('./models'),
+        faceapi.nets.faceRecognitionNet.loadFromUri('./models'),
+        faceapi.nets.ageGenderNet.loadFromUri('./models'),
+    ])
+```
+
+### Stock photos I use, 2 Ronaldo and 1 Musk:
+
+- https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Cristiano_Ronaldo_playing_for_Al_Nassr_FC_against_Persepolis%2C_September_2023_%28cropped%29.jpg/220px-Cristiano_Ronaldo_playing_for_Al_Nassr_FC_against_Persepolis%2C_September_2023_%28cropped%29.jpg
+
+- https://upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Cristiano_Ajax.jpg/170px-Cristiano_Ajax.jpg
+
+- https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Elon_Musk_Colorado_2022_%28cropped2%29.jpg/220px-Elon_Musk_Colorado_2022_%28cropped2%29.jpg
+
+- https://upload.wikimedia.org/wikipedia/commons/thumb/7/71/JordanSmithWorthy2.jpg/170px-JordanSmithWorthy2.jpg
+
+- https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/Michael_Jordan_in_2014.jpg/220px-Michael_Jordan_in_2014.jpg

+ 4 - 0
src/face-api-js-starter-main/server.js

@@ -0,0 +1,4 @@
+const express = require('express')
+const app = express()
+app.use(express.static('public')) //serve our files in public statically
+app.listen(5000)

+ 17 - 0
src/modules/aigc/agent/agent-shige/agent-shige-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { AgentShigePage } from './agent-shige.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: AgentShigePage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class AgentShigePageRoutingModule {}

+ 20 - 0
src/modules/aigc/agent/agent-shige/agent-shige.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { AgentShigePageRoutingModule } from './agent-shige-routing.module';
+
+import { AgentShigePage } from './agent-shige.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    AgentShigePageRoutingModule
+  ],
+  declarations: [AgentShigePage]
+})
+export class AgentShigePageModule {}

+ 41 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.html

@@ -0,0 +1,41 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>
+      诗词创作
+    </ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-item>
+    <ion-input placeholder="关键词" [(ngModel)]="shigeOptions.keywords"></ion-input>
+  </ion-item>
+  <ion-item>
+    <ion-input placeholder="创作灵感" [(ngModel)]="shigeOptions.content"></ion-input>
+  </ion-item>
+  <ion-item>
+    <ion-select label="诗歌格律" placeholder="格律" [(ngModel)]="shigeOptions.type">
+      <ion-select-option value="不限制">不限制</ion-select-option>
+      <ion-select-option value="七言绝句">七言绝句</ion-select-option>
+      <ion-select-option value="五言律诗">五言律诗</ion-select-option>
+      <ion-select-option value="藏头诗">藏头诗</ion-select-option>
+      <ion-select-option value="宋词">藏头诗</ion-select-option>
+    </ion-select>
+  </ion-item>
+  <ion-item>
+    <ion-select label="诗歌主题" placeholder="主题" [(ngModel)]="shigeOptions.theme">
+      <ion-select-option value="不限制">不限制</ion-select-option>
+      <ion-select-option [value]="item" *ngFor="let item of themeList">{{item}}</ion-select-option>
+    </ion-select>
+  </ion-item>
+  <ion-button expand="block" (click)="sendMessage()">发送</ion-button>
+
+  <ion-card *ngFor="let message of messageList">
+    <ion-card-header>
+      {{message?.role}}
+    </ion-card-header>
+    <ion-card-content>
+      {{ message?.content }}
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 0 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.scss


+ 17 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AgentShigePage } from './agent-shige.page';
+
+describe('AgentShigePage', () => {
+  let component: AgentShigePage;
+  let fixture: ComponentFixture<AgentShigePage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(AgentShigePage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 91 - 0
src/modules/aigc/agent/agent-shige/agent-shige.page.ts

@@ -0,0 +1,91 @@
+import { Component, OnInit } from '@angular/core';
+// 引用FmodeChatCompletion类
+import { TestRxjsChatCompletion,TestRxjsChatMessage } from '../../chat/class-rxjs-chat-completion';
+import Parse from "parse";
+@Component({
+  selector: 'app-agent-shige',
+  templateUrl: './agent-shige.page.html',
+  styleUrls: ['./agent-shige.page.scss'],
+})
+export class AgentShigePage implements OnInit {
+  messageList:Array<TestRxjsChatMessage> = []
+  shigeOptions:any ={
+    content:"",
+    type:"不限制",
+    theme:"不限制",
+  }
+  themeList = ['羁旅思乡诗','爱情闺怨诗','咏史怀古诗','咏物言志诗','送别怀人诗','边塞征战诗','山水田园诗']
+
+  constructor() { 
+  }
+
+  ngOnInit() {
+  }
+  sendMessage(){
+    /*
+4. 风格:选择风格,例如“豪放”或“婉约”。
+5. 情感:描述诗中应表达的情感,例如“喜悦”、“忧愁”、“思念”等。
+
+    */
+    let GuhiPromoptTemplate = `
+你是一位中国的古代诗人,擅长文言文,和现代词汇转古词语经验和技巧,并且精通各类主题、格律的诗词。请根据以下要求创作一首诗:
+1. 主题:${this.shigeOptions?.theme}。
+2. 格律:${this.shigeOptions?.type}。
+3. 关键词:${this.shigeOptions?.keywords}。
+4. 创意灵感:${this.shigeOptions?.content}。
+请根据以上要求创作一首古代诗词,并且格式要严格,有必要的情况下,可以牺牲一些关键词原意。
+
+请开始创作,并按照以下格式返回
+题目:
+内容:
+简介:
+    `
+    this.messageList.push({
+      role:"user",
+      content: GuhiPromoptTemplate
+    })
+    
+    // messageList在competion内部,已经赋予了完整的message
+    // 下方暴露出来的可订阅内容,主要是用于关键字过滤,或者其他开发逻辑的续写
+    let resultStr = ""
+    let testChatCompletion = new TestRxjsChatCompletion(this.messageList);
+    testChatCompletion.createCompletionByStream({model:"fmode-3.6-16k"}).subscribe({
+        next: ({ content, cumulativeContent, done }) => {
+          resultStr = cumulativeContent
+            console.log(`Content: ${content}`);
+            console.log(`Cumulative Content: ${cumulativeContent}`);
+            if (done) {
+                console.log('Stream completed');
+            }
+        },
+        error: err => console.error(err),
+        complete: () => {
+          // 诗歌创建完成:正则表达式,匹配诗歌json内容
+          console.log("原文",resultStr)
+                    
+          let pattern = /题目:\s*(.*?)\s*内容:\s*(.*?)\s*简介:\s*(.*)/;
+          let match = resultStr.match(pattern);
+
+          if (match) {
+            let gushi:any = {}
+              gushi.title = match[1];
+              gushi.content = match[2];
+              gushi.intro = match[3];
+              gushi.source = "AI创作";
+              console.log(`题目: ${gushi.title}`);
+              console.log(`内容: ${gushi.content}`);
+              console.log(`简介: ${gushi.intro}`);
+              console.log(gushi);
+              let Shige = Parse.Object.extend("Shige");
+              let sg = new Shige();
+              sg.set(gushi);
+              sg.save();
+          } else {
+              console.log("未能匹配到任何内容");
+          }
+        }
+    });
+
+  }
+
+}

+ 16 - 0
src/modules/aigc/aigc-routing.module.ts

@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+const routes: Routes = [
+  {path: 'chat', loadChildren: () => import('./chat/chat.module').then(mod => mod.ChatPageModule)},
+  {
+    path: 'agent/shige',
+    loadChildren: () => import('./agent/agent-shige/agent-shige.module').then( m => m.AgentShigePageModule)
+  },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class AigcRoutingModule { }

+ 13 - 0
src/modules/aigc/aigc.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { AigcRoutingModule } from './aigc-routing.module';
+
+
+@NgModule({
+  declarations: [],
+  imports: [
+    CommonModule,
+    AigcRoutingModule
+  ]
+})
+export class AigcModule { }

+ 17 - 0
src/modules/aigc/chat/chat-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { ChatPage } from './chat.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: ChatPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class ChatPageRoutingModule {}

+ 22 - 0
src/modules/aigc/chat/chat.data.md

@@ -0,0 +1,22 @@
+# 原始接收数据
+
+``` sh
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":""},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"从"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"前"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":","},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"有"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"一个"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"小"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"村"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"庄"},"logprobs":null,"finish_reason":null}]}
+# ...............
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"充"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"实"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"和"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"幸"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"福"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"。"},"logprobs":null,"finish_reason":null}]}
+data: {"id":"chatcmpl-CMMUQdD0y8Ug6IJTnxLFAwsve42QX","object":"chat.completion.chunk","created":1720420748,"model":"gpt-3.5-turbo-0125","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]}
+data: [DONE]
+```

+ 20 - 0
src/modules/aigc/chat/chat.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { ChatPageRoutingModule } from './chat-routing.module';
+
+import { ChatPage } from './chat.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    ChatPageRoutingModule
+  ],
+  declarations: [ChatPage]
+})
+export class ChatPageModule {}

+ 24 - 0
src/modules/aigc/chat/chat.page.html

@@ -0,0 +1,24 @@
+<ion-header>
+  <ion-toolbar>
+    <ion-title>
+      AIGC 消息发送
+    </ion-title>
+  </ion-toolbar>
+</ion-header>
+
+<ion-content>
+  <ion-item>
+    <ion-input placeholder="输入消息" [(ngModel)]="userInput"></ion-input>
+  </ion-item>
+
+  <ion-button expand="block" (click)="sendMessage()">发送</ion-button>
+
+  <ion-card *ngFor="let message of messageList">
+    <ion-card-header>
+      {{message?.role}}
+    </ion-card-header>
+    <ion-card-content>
+      {{ message?.content }}
+    </ion-card-content>
+  </ion-card>
+</ion-content>

+ 0 - 0
src/modules/aigc/chat/chat.page.scss


+ 17 - 0
src/modules/aigc/chat/chat.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ChatPage } from './chat.page';
+
+describe('ChatPage', () => {
+  let component: ChatPage;
+  let fixture: ComponentFixture<ChatPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ChatPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 119 - 0
src/modules/aigc/chat/chat.page.ts

@@ -0,0 +1,119 @@
+import { Component, OnInit } from '@angular/core';
+// 引用FmodeChatCompletion类
+import { TestChatCompletion, TestChatMessage } from './class-test-chat-completion';
+import { TestRxjsChatCompletion } from './class-rxjs-chat-completion';
+
+@Component({
+  selector: 'app-chat',
+  templateUrl: './chat.page.html',
+  styleUrls: ['./chat.page.scss'],
+})
+export class ChatPage implements OnInit {
+  messageList:Array<TestChatMessage> = []
+  userInput:string = ""
+
+  Postjson = {
+    "面部特征分析": {
+      "三庭比例": {
+        "前庭": "145.88",
+        "中庭": "173.07",
+        "下庭": "63.76",
+        "分析": "前庭较短,说明额头相对较窄;中庭较长,显示脸中部较长,鼻子和颧骨区域比较突出;下庭较短,表明下巴较短。",
+        "形象设计建议": "可以选择有刘海的发型,如空气刘海,增加额头的宽度。同时选择有层次感的发型,平衡脸部中庭的长度。使用高光在额头部位,增加视觉上的额头长度。同时在鼻子两侧使用阴影,缩短中庭的长度感。"
+      },
+      "五眼比例": {
+        "比例": "1035.03",
+        "分析": "五眼比例较大,说明眼睛之间的间距相对较宽。",
+        "形象设计建议": "可以选择猫眼妆,通过延长眼线和使用深色眼影在眼角部分,增加眼睛的聚拢感。眉毛可以修剪得靠近眼睛的位置,增加眼睛的集中度。"
+      },
+      "脸型": {
+        "类型": "长形脸",
+        "分析": "长形脸说明脸部长度较大,相对宽度较窄。",
+        "形象设计建议": "选择中长发或短发,卷发或大波浪可以增加脸部的宽度,减少脸部长度的视觉效果。侧分刘海也有助于缩短脸部长度。在脸颊中央使用腮红,增加脸部的饱满感,平衡脸部长度。"
+      },
+      "眉形": {
+        "类型": "浓眉",
+        "分析": "浓眉说明眉毛较为浓密,需要适当修饰使其更加整洁有型。",
+        "形象设计建议": "保持自然的浓眉,适当修饰眉毛的形状,避免过于锐利的眉形。可以使用眉刷和眉笔进行细致的修整,使其更加有型。"
+      },
+      "唇形": {
+        "类型": "薄唇",
+        "分析": "薄唇说明嘴唇较为纤薄,需要增加唇部的丰满感。",
+        "形象设计建议": "使用唇线笔勾勒出唇部轮廓,稍微超出自然唇线,使唇形显得更加丰满。选择光泽感强的唇膏或唇蜜,增加唇部的立体感。"
+      },
+      "眼睛形状": {
+        "类型": "圆形眼",
+        "分析": "圆形眼说明眼睛较为圆润,有一定的可爱感。",
+        "形象设计建议": "可以尝试猫眼妆或烟熏妆,增加眼部的深邃感。使用眼线笔或眼线液拉长眼尾,适当加粗眼线,增强眼部轮廓感。选择哑光和珠光结合的眼影,可以增加眼部的层次感。"
+      },
+      "鼻形": {
+        "类型": "鹰钩鼻",
+        "分析": "鹰钩鼻说明鼻子的弧度较大,有一定的锐利感。",
+        "形象设计建议": "通过高光和修容来调整鼻子的视觉效果。在鼻梁和鼻头部位使用高光,在鼻翼两侧使用阴影,能够柔化鼻子的线条。"
+      },
+      "颧骨": {
+        "类型": "颧骨平",
+        "分析": "颧骨平说明颧骨不突出,面部轮廓较为平坦。",
+        "形象设计建议": "在颧骨部位使用阴影,增加颧骨的立体感,使面部轮廓更加分明。"
+      },
+      "下巴": {
+        "类型": "短下巴",
+        "分析": "短下巴说明下巴较为短小,需要增加下巴的长度感。",
+        "形象设计建议": "在下巴部位使用高光,增加视觉上的下巴长度感,使面部比例更加和谐。"
+      },
+      "额头": {
+        "类型": "窄额头",
+        "分析": "窄额头说明额头较为窄小,需要增加额头的宽度感。",
+        "形象设计建议": "选择有刘海的发型,如空气刘海,增加额头的宽度感。"
+      },
+      "脸颊": {
+        "类型": "瘦削脸颊",
+        "分析": "瘦削脸颊说明脸颊较为瘦削,需要增加脸颊的饱满感。",
+        "形象设计建议": "使用腮红增加脸颊的饱满度,选择暖色调的腮红(如桃粉色、珊瑚色),从笑肌处轻扫至太阳穴,制造出健康红润的感觉。"
+      }
+    }
+  };
+
+  Postprompt={
+    "现在你是一个形象设计大师,我的脸部特征是这些:" : ",三庭五眼:"+"145.88,173.07, 63.76"+",五眼比例:"+"1035.03"+",脸型:"+"长形脸"+",眉形: "+"浓眉"+",唇形: "+"薄唇"+",眼睛形状: "+"圆形眼"+",鼻形:"+"鹰钩鼻"+",颧骨:"+"颧骨平"+",下巴: "+"短下巴"+",额头: "+"窄额头"+",脸颊: "+"瘦削脸颊"+"。请你先分析说明每个特征分类的特点,然后给出形象设计意见,并将结果转换成json的格式。例如"+JSON.stringify(this.Postjson)
+  }
+  PostpromptString = JSON.stringify(this.Postprompt);
+  completion:TestChatCompletion
+
+  
+  constructor() { 
+    // 测试类:纯fetch读取http event stream数据
+    this.completion = new TestChatCompletion(this.messageList)
+
+    // 测试类:rxjs封装的可观察对象
+  }
+
+  ngOnInit() {
+  }
+  sendMessage(){
+    this.messageList.push({
+      role:"user",
+      content:this.PostpromptString,
+    })
+    this.userInput = ""
+    
+    // this.completion.createCompletionByStream()
+
+    // messageList在competion内部,已经赋予了完整的message
+    // 下方暴露出来的可订阅内容,主要是用于关键字过滤,或者其他开发逻辑的续写
+    let testChatCompletion = new TestRxjsChatCompletion(this.messageList);
+    testChatCompletion.createCompletionByStream().subscribe({
+        next: ({ content, cumulativeContent, done }) => {
+            console.log(`Content: ${content}`);
+            console.log(`Cumulative Content: ${cumulativeContent}`);
+            if (done) {
+                console.log('Stream completed');
+            }
+        },
+        error: err => console.error(err),
+        complete: () => console.log('Observable completed')
+    });
+
+  }
+
+}

+ 147 - 0
src/modules/aigc/chat/class-rxjs-chat-completion.ts

@@ -0,0 +1,147 @@
+import { Observable, from, of } from 'rxjs';
+import { switchMap, map, catchError, finalize } from 'rxjs/operators';
+import { ResultsPage } from '../../study/results/results.page';
+import{ParseObject} from'../../study/results/results-post'
+import Parse from "parse"
+Parse.initialize("dev");
+Parse.serverURL="http://web2023.fmode.cn:9999/parse";
+
+export interface TestRxjsChatMessage {
+    role: string;
+    content: string;
+}
+
+export class TestRxjsChatCompletion {
+    messageList: Array<TestRxjsChatMessage>;
+    messageAiReply = "";
+    lastCompletionContent:string = ""; // 新增属性以保存最后的对话结果
+
+
+    async SaveMessage()
+    {
+        let savemessage = new ResultsPage();
+        let chatResult=this.lastCompletionContent;
+        console.log(this.lastCompletionContent)
+        console.log("savemessage.currentObjectId",ResultsPage.currentObjectId)
+        async  function  SaveChatResult() 
+        {
+          let YCX:any = new ParseObject("MagicMirror")
+             YCX = await YCX.get(ResultsPage.currentObjectId)
+             YCX.set(
+              {
+                result:chatResult,    
+               }    )
+               YCX.save2(ResultsPage.currentObjectId);
+            // console.log(YCX);
+        }
+        SaveChatResult();
+    }
+    constructor(messageList: Array<TestRxjsChatMessage>) {
+        this.messageList = messageList;
+    }
+
+    createCompletionByStream(options?:{
+        model?:string
+    }): Observable<{ content: string, cumulativeContent: string, done: boolean }> {
+        const token = localStorage.getItem("token");
+        const bodyJson = {
+            "token": `Bearer ${token}`,
+            "messages": this.messageList,
+            "model": options?.model || "fmode-3.6-16k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "top_p": 1,
+            "stream": true
+        };
+
+        return from(fetch("https://test.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+            "headers": {
+                "accept": "text/event-stream",
+                "sec-fetch-dest": "empty",
+                "sec-fetch-mode": "cors",
+                "sec-fetch-site": "same-site"
+            },
+            "referrer": "https://ai.fmode.cn/",
+            "referrerPolicy": "strict-origin-when-cross-origin",
+            "body": JSON.stringify(bodyJson),
+            "method": "POST",
+            "mode": "cors",
+            "credentials": "omit"
+        })).pipe(
+            switchMap(response => {
+                const reader = response.body?.getReader();
+                if (!reader) {
+                    throw new Error("Failed to get the response reader.");
+                }
+                const decoder = new TextDecoder();
+                let buffer = "";
+                let messageAiReply = "";
+                let messageIndex = this.messageList.length;
+
+                return new Observable<{ content: string, cumulativeContent: string, done: boolean }>(observer => {
+                    const read = () => {
+                        reader.read().then(({ done, value }) => {
+                            if (done) {
+                                observer.next({ content: "", cumulativeContent: messageAiReply, done: true });
+                                observer.complete();
+                                return;
+                            }
+
+                            buffer += decoder.decode(value);
+                            let messages = buffer.split("\n");
+
+                            for (let i = 0; i < messages.length - 1; i++) {
+                                let message = messages[i];
+                                let dataText = message.replace("data: ", "");
+
+                                if (dataText.startsWith("{")) {
+                                    try {
+                                        let dataJson = JSON.parse(dataText);
+                                        let content = dataJson?.choices?.[0]?.delta?.content || "";
+                                        messageAiReply += content;
+                                        this.lastCompletionContent = messageAiReply;
+                                        this.messageList[messageIndex] = {
+                                            role: "assistant",
+                                            content: messageAiReply
+                                        };
+                                        observer.next({ content, cumulativeContent: messageAiReply, done: false });
+                                    } catch (err) { }
+                                }
+
+                                if (dataText.startsWith("[")) {
+                                    this.messageList[messageIndex] = {
+                                        role: "assistant",
+                                        content: messageAiReply
+                                    };
+                                    observer.next({ content: "", cumulativeContent: messageAiReply, done: true });
+
+                                }
+
+                                buffer = buffer.slice(message.length + 1);
+                            }
+
+                            read();
+                        }).catch(err => observer.error(err));
+                    };
+
+                    read();
+                });
+            }),
+            catchError(err => {
+                console.error(err);
+                return of({ content: "", cumulativeContent: "", done: true });
+            }),
+            finalize(() => {
+                
+                console.log("Stream completed");
+                console.log(this.lastCompletionContent)
+                this.SaveMessage()
+                
+                                
+            })
+            
+        );
+    }
+    
+}

+ 103 - 0
src/modules/aigc/chat/class-test-chat-completion.ts

@@ -0,0 +1,103 @@
+/*
+  案例:纯fetch读取http event stream数据
+*/
+export interface TestChatMessage{
+    role:string
+    content:string
+}
+export class TestChatCompletion{
+messageList:Array<TestChatMessage>
+constructor(messageList:Array<TestChatMessage>){
+    this.messageList = messageList
+}
+async createCompletionByStream() {
+
+let token = localStorage.getItem("token");
+let bodyJson = {
+  "token": `Bearer ${token}`,
+  "messages": this.messageList,
+  "model": "fmode-3.6-16k",
+  "temperature": 0.5,
+  "presence_penalty": 0,
+  "frequency_penalty": 0,
+  "top_p": 1,
+  "stream":false
+};
+
+let response = await fetch("https://test.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+  "headers": {
+    "accept": "text/event-stream",
+    "sec-fetch-dest": "empty",
+    "sec-fetch-mode": "cors",
+    "sec-fetch-site": "same-site"
+  },
+  "referrer": "https://ai.fmode.cn/",
+  "referrerPolicy": "strict-origin-when-cross-origin",
+  "body": JSON.stringify(bodyJson),
+  "method": "POST",
+  "mode": "cors",
+  "credentials": "omit"
+});
+
+let messageAiReply = ""
+let messageIndex = this.messageList.length
+let reader = response.body?.getReader();
+if (!reader) {
+  throw new Error("Failed to get the response reader.");
+}
+
+let decoder = new TextDecoder();
+let buffer = "";
+
+while (true) {
+  let { done, value } = await reader.read();
+  if (done) {
+    break;
+  }
+
+  buffer += decoder.decode(value);
+
+  // Split the buffer by newlines to get individual messages
+  let messages = buffer.split("\n");
+
+  // Process each message
+  for (let i = 0; i < messages.length - 1; i++) {
+    let message = messages[i];
+
+    // Process the message as needed
+    /**
+     * data: {"id":"chatcmpl-y2PLKqPDnwAFJIj2L5aqdH5TWK9Yv","object":"chat.completion.chunk","created":1696770162,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
+     * data: {"id":"chatcmpl-y2PLKqPDnwAFJIj2L5aqdH5TWK9Yv","object":"chat.completion.chunk","created":1696770162,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
+     * data: [DONE]
+     */
+    let dataText = message.replace("data:\ ","")
+    console.log(dataText)
+    if(dataText.startsWith("{")){
+      try{
+        let dataJson = JSON.parse(dataText)
+        console.log(dataJson)
+        messageAiReply += dataJson?.choices?.[0]?.delta?.content || ""
+        this.messageList[messageIndex] = {
+          role:"assistant",
+          content:messageAiReply
+        }
+      }catch(err){}
+    }
+    if(dataText.startsWith("[")){
+      console.log(message)
+      console.log("完成")
+      this.messageList[messageIndex] = {
+        role:"assistant",
+        content:messageAiReply
+      }
+      messageAiReply = ""
+    }
+    // Parse the message as JSON
+    // let data = JSON.parse(message);
+
+    // Clear the processed message from the buffer
+    buffer = buffer.slice(message.length + 1);
+  }
+}
+}
+}

+ 30 - 0
src/modules/babylon/README.md

@@ -0,0 +1,30 @@
+# WebGPU Babylon教学案例库
+
+
+
+# 模型素材
+- 中国城市lowpoly
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-city-lowpoly/gltf/
+    - FILE scene.gltf
+- 中国象棋
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-chess/gltf/
+    - FILE scene.gltf
+- 古风少女
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/ancient-chinese-woman/gltf/
+    - FILE scene.gltf
+- 古风少年
+    - https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/ancient-chinese-man/README.md
+- 卡通异瞳少女
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/vtuber-selen/gltf/
+    - FILE scene.gltf
+- 中国老杯子
+    - DIR https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-iron-cup/
+    - FILE scene.gltf
+
+## 上传格式
+- OBS对象KEY
+    - /storage/3d/model/<model_name>/gltf/scene.gltf
+- OBS命令行
+    - obsutil sync ../model/ obs://nova-cloud/storage/3d/model/
+- 本地临时存储
+    - ~/Downloads/model/<model_name>/gltf/scene.gltf

+ 12 - 0
src/modules/babylon/pages/case-babylon/README.md

@@ -0,0 +1,12 @@
+# Babylon.js使用示例
+
+- 官方网站 https://doc.babylonjs.com/
+
+# 依赖安装
+- npm安装 https://doc.babylonjs.com/setup/frameworkPackages/es6Support
+
+``` bash
+npm install @babylonjs/core -S
+npm install @babylonjs/materials -S
+npm install @babylonjs/loaders -S
+```

+ 17 - 0
src/modules/babylon/pages/case-babylon/case-babylon-routing.module.ts

@@ -0,0 +1,17 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { CaseBabylonPage } from './case-babylon.page';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: CaseBabylonPage
+  }
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule],
+})
+export class CaseBabylonPageRoutingModule {}

+ 20 - 0
src/modules/babylon/pages/case-babylon/case-babylon.module.ts

@@ -0,0 +1,20 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { IonicModule } from '@ionic/angular';
+
+import { CaseBabylonPageRoutingModule } from './case-babylon-routing.module';
+
+import { CaseBabylonPage } from './case-babylon.page';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    IonicModule,
+    CaseBabylonPageRoutingModule
+  ],
+  declarations: [CaseBabylonPage]
+})
+export class CaseBabylonPageModule {}

+ 6 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.html

@@ -0,0 +1,6 @@
+<div class="list">
+  <div (click)="showCharacter(item?.name)" class="character" *ngFor="let item of CharacterMeshList">
+    <h1>{{item?.name}}</h1>
+  </div>
+</div>
+<canvas id="renderCanvas" touch-action="none"></canvas>

+ 43 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.scss

@@ -0,0 +1,43 @@
+// 调试面板
+#scene-explorer-host {
+    position: absolute !important;
+}
+#sceneExplorer{
+    position:absolute!important;
+    height:100vh!important;
+}
+.babylonDebugLayer {
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    width: 300px; /* 调试层的宽度 */
+    background-color: rgba(255, 255, 255, 0.8); /* 背景颜色和透明度 */
+    z-index: 20; /* 确保调试层在最顶层 */
+    overflow: auto; /* 如果内容过多,添加滚动条 */
+  }
+  
+  .babylonDebugLayer .content {
+    padding: 10px;
+  }
+
+// 展示区
+#renderCanvas {
+    width: 100vw;
+    height: 100vh;
+    display: block;
+    font-size: 0;
+    z-index: 1;
+}
+
+.list{
+    position: absolute;
+    top:20px;
+    z-index: 10;
+    width: 100vw;
+    display: flex;
+    justify-content: center;
+    .character{
+        
+    }
+}

+ 17 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { CaseBabylonPage } from './case-babylon.page';
+
+describe('CaseBabylonPage', () => {
+  let component: CaseBabylonPage;
+  let fixture: ComponentFixture<CaseBabylonPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CaseBabylonPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 171 - 0
src/modules/babylon/pages/case-babylon/case-babylon.page.ts

@@ -0,0 +1,171 @@
+import { Component, OnInit } from '@angular/core';
+import * as BABYLON from "@babylonjs/core/Legacy/legacy";
+import { Engine, Scene } from "@babylonjs/core";
+import { FreeCamera } from "@babylonjs/core/Cameras/freeCamera";
+import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight";
+import { Vector3 } from "@babylonjs/core/Maths/math.vector";
+import { CreateGround } from "@babylonjs/core/Meshes/Builders/groundBuilder";
+import { CreateSphere } from "@babylonjs/core/Meshes/Builders/sphereBuilder";
+import { GridMaterial } from "@babylonjs/materials/grid/gridMaterial";
+
+import "@babylonjs/loaders/glTF";
+
+@Component({
+  selector: 'app-case-babylon',
+  templateUrl: './case-babylon.page.html',
+  styleUrls: ['./case-babylon.page.scss'],
+})
+export class CaseBabylonPage implements OnInit {
+  private engine: BABYLON.Engine|undefined;
+  private scene: BABYLON.Scene|undefined;
+
+  CharacterMap:any = {}
+  CharacterMeshList = [
+    {name:"古风少女",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/ancient-chinese-woman/gltf/",
+    filePath:"scene.gltf"},
+    {name:"中国象棋",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-chess/gltf/",
+    filePath:"scene.gltf"},
+    {name:"卡通异瞳少女",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/vtuber-selen/gltf/",
+    filePath:"scene.gltf"},
+    {name:"中国老杯子",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-iron-cup/",
+    filePath:"scene.gltf"},
+    {name:"中国城市lowpoly",
+    dirPath:"https://nova-cloud.obs.cn-south-1.myhuaweicloud.com/storage/3d/model/chinese-city-lowpoly/gltf/",
+    filePath:"scene.gltf"},
+
+  ]
+  constructor() { }
+
+  ngOnInit() {
+    this.initBabylon()
+  }
+
+  initBabylon(){
+    // Get the canvas element from the DOM.
+    const canvas:any = document.getElementById("renderCanvas");
+    // Set the canvas size to match the device pixel ratio
+    const devicePixelRatio = window.devicePixelRatio || 1;
+    canvas.width = window.innerWidth * devicePixelRatio;
+    canvas.height = window.innerHeight * devicePixelRatio;
+    this.engine = new BABYLON.Engine(canvas, true);
+    this.scene = new BABYLON.Scene(this.engine);
+
+    // Create a basic light, aiming 0,1,0 - meaning, to the sky.
+    const light = new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 1, 0), this.scene);
+
+    // 加载粒子效果覆盖地面
+    this.loadGroundParticle()
+    
+    // 设置初始相机
+    // Add an ArcRotateCamera to the scene and attach it to the canvas
+    // let initPositon = new BABYLON.Vector3(0,12,0)
+    let initPositon = new BABYLON.Vector3(0,0,0)
+    let beta = Math.PI / 2.5 // 镜头初始 纬度 Math.PI / 3 斜下45度
+    let radius = 20 // 镜头初始半径
+    const camera = new BABYLON.ArcRotateCamera('camera1', Math.PI / 2, beta, radius, initPositon, this.scene);
+    camera.attachControl(canvas, true);
+
+    // Load the FBX model
+
+    this.CharacterMeshList.forEach(character=>{
+      BABYLON.SceneLoader.ImportMesh('', character.dirPath, character.filePath, this.scene, (meshes, particleSystems, skeletons) => {
+        console.log(meshes)
+        // characterMesh.isVisible
+        meshes.forEach(mesh=>mesh.isVisible=false)
+        this.CharacterMap[character?.name] = {
+          meshes:meshes,
+          particleSystems:particleSystems,
+          skeletons:skeletons
+        }
+        if (skeletons.length > 0) {
+          this.scene?.beginAnimation(skeletons[0], 0, 100, true);
+        }
+      });
+    })
+    setTimeout(() => {
+      this.showCharacter("机器人")
+    }, 3000);
+    
+
+    // Register a render loop to repeatedly render the scene.
+    this.engine.runRenderLoop(() => {
+      this.scene?.render();
+    });
+
+    // 开启调试层,用于抓取角度
+    this.scene.debugLayer.show();
+
+    // Watch for browser/canvas resize events
+    window.addEventListener('resize', () => {
+      this.engine?.resize();
+    });
+  }
+
+  showCharacter(name?:string){
+    if(!name) return
+
+    Object.keys(this.CharacterMap).forEach(tempname=>{
+      let character = this.CharacterMap[tempname];
+      if(tempname==name){
+        if(character.meshes?.length){
+          character.meshes.forEach((mesh:any)=>mesh.isVisible=true)
+        }
+      }else{
+        if(character.meshes?.length){
+          character.meshes.forEach((mesh:any)=>mesh.isVisible=false)
+        }
+      }
+    })
+
+   
+  }
+  loadGroundParticle(){
+      if(!this.scene) return
+      // 创建粒子系统
+      const particleSystem = new BABYLON.ParticleSystem("particles", 2000, this.scene);
+
+      // 纹理
+      particleSystem.particleTexture = new BABYLON.Texture("textures/flare.png", this.scene);
+
+      // 发射器
+      particleSystem.emitter = new BABYLON.Vector3(0, 0, 0); // 发射器位置
+      particleSystem.minEmitBox = new BABYLON.Vector3(-300, 0, -300); // 最小发射范围
+      particleSystem.maxEmitBox = new BABYLON.Vector3(300, 0, 300); // 最大发射范围
+
+      // 粒子颜色
+      particleSystem.color1 = new BABYLON.Color4(1, 1, 1, 1);
+      particleSystem.color2 = new BABYLON.Color4(0.5, 0.5, 1, 1);
+      particleSystem.colorDead = new BABYLON.Color4(0, 0, 0.2, 0.5);
+
+      // 粒子大小
+      particleSystem.minSize = 0.1;
+      particleSystem.maxSize = 0.5;
+
+      // 粒子生命周期
+      particleSystem.minLifeTime = 0.3;
+      particleSystem.maxLifeTime = 1.5;
+
+      // 发射速率
+      particleSystem.emitRate = 1000;
+
+      // 粒子方向
+      particleSystem.direction1 = new BABYLON.Vector3(-1, 1, -1);
+      particleSystem.direction2 = new BABYLON.Vector3(1, 1, 1);
+
+      // 重力
+      particleSystem.gravity = new BABYLON.Vector3(0, -9.81, 0);
+
+      // 粒子速度
+      particleSystem.minEmitPower = 0.5;
+      particleSystem.maxEmitPower = 1.5;
+      particleSystem.updateSpeed = 0.01;
+
+      // 开始粒子系统
+      particleSystem.start();
+  }
+
+}

+ 79 - 0
src/modules/contact/README.md

@@ -0,0 +1,79 @@
+# 通讯模块
+
+# 项目结构
+- chat 对话页面
+- session 历史会话
+- contact-list 通讯录列表
+
+
+# 对话模块
+- 功能简介:两个用户互相发消息,接收消息,查看消息历史记录
+
+## 数据范式
+- _User 用户表
+    - objectId
+    - username 用户名
+    - mobile 手机号
+    - nickname 昵称
+- Contact 通讯好友的表
+    - from Pointer<_User>
+    - to Pointer<_User>
+- Message 消息表
+    - sendUser Pointer<_User>
+    - receiveUser Pointer<_User>
+    - contentJson 消息对象 符合各类消息格式
+    - isRead Boolean 消息已读
+    - isCancel Boolean 消息撤回
+
+思考:图片链接和图片消息的区别,和表达方式?
+图片链接,本质上是文本消息(基于字符串)
+图片消息,本质上是结构化消息 {type:"image",imgUrl:"https://file-cloud.fmode.cn/E4KpGvTEto/20230822/3mkf41033623275.png"}
+
+### 消息内容规范(参考GPT多模态响应)
+- 参考文档:https://ai.fmode.cn/chat/share/nxZMG3CZrd
+
+普通文本消息:
+
+{
+      "role": "user",
+      "content": "Here is the text: 'The quick brown fox jumps over the lazy dog.'"
+}
+
+ 图片消息
+{
+    "role": "user",
+    "content": {
+    "type": "image",
+    "data": {
+        "url": "https://example.com/image.jpg",
+        "alt_text": "A quick brown fox jumping over a lazy dog."
+    }
+    }
+}
+
+音频消息
+
+{
+    "role": "user",
+    "content": {
+    "type": "audio",
+    "data": {
+        "url": "https://example.com/audio.mp3",
+        "alt_text": "An audio clip of a quick brown fox jumping over a lazy dog."
+    }
+    }
+}
+
+### 业务逻辑
+
+#### 发送消息
+- 进入会话页面
+    - 无记录:输入用户昵称添加好友,再进行对话
+    - 有记录:点击历史会话进入
+- 向微服务创建一个Message,sendUser为自身,receiveUser为联系人
+
+#### 接收消息
+- 查询,receiveUser为自身,sendUser为会话窗口联系人的所有Message
+
+#### 会话列表
+- 查询,receiveUser为自身,所有Message,并每个receiver只显示最新一条

+ 13 - 0
src/modules/contact/chat/chat-routing.module.ts

@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { ChatPage } from './chat.page';
+// import { Chat1Page } from './tab1 copy/tab11.page';
+const routes: Routes = [
+  { path: '', component: ChatPage },
+];
+
+@NgModule({
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
+})
+export class ChatPageRoutingModule {}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików