Prechádzať zdrojové kódy

new file: .editorconfig
new file: .gitignore
new file: .prettierrc
new file: angular.json
new file: body.json
new file: package-lock.json
new file: package.json
new file: payload-stream.json
new file: payload.json
new file: proxy.conf.json
new file: server.js
new file: src/app/app.config.ts
new file: src/app/app.css
new file: src/app/app.html
new file: src/app/app.routes.ts
new file: src/app/app.spec.ts
new file: src/app/app.ts
new file: src/app/components/api-test/api-test.component.ts
new file: src/app/components/debug-panel/debug-panel.component.ts
new file: src/app/components/workflow/workflow.component.ts
new file: src/app/fetch
new file: src/app/models/types.ts
new file: src/app/pipes/file-size.pipe.ts
new file: src/app/pipes/number-format.pipe.ts
new file: src/app/pipes/time-format.pipe.ts
new file: src/app/services/douyin-api.service.ts
new file: src/app/services/douyin.service.ts
new file: src/app/services/jimeng-api.service.ts
new file: src/app/services/jimeng-workflow.service.ts
new file: src/app/services/jimeng.service.ts
new file: src/app/services/llm.service.ts
new file: src/app/services/workflow.service.ts
new file: src/environments/environment.prod.ts
new file: src/environments/environment.ts
new file: src/index.html
new file: src/main.ts
new file: src/styles.css
new file: src/video/manifest.json
new file: stream.txt
new file: tsconfig.app.json
new file: tsconfig.json
new file: tsconfig.spec.json

Yi Jiarui 4 mesiacov pred
rodič
commit
9e4914a8bf

+ 17 - 0
.editorconfig

@@ -0,0 +1,17 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+ij_typescript_use_double_quotes = false
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false

+ 47 - 0
.gitignore

@@ -0,0 +1,47 @@
+# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+.env
+.env.*
+!.env.example
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/mcp.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+__screenshots__/
+
+# System files
+.DS_Store
+Thumbs.db

+ 12 - 0
.prettierrc

@@ -0,0 +1,12 @@
+{
+  "printWidth": 100,
+  "singleQuote": true,
+  "overrides": [
+    {
+      "files": "*.html",
+      "options": {
+        "parser": "angular"
+      }
+    }
+  ]
+}

+ 79 - 0
angular.json

@@ -0,0 +1,79 @@
+{
+  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+  "version": 1,
+  "cli": {
+    "packageManager": "npm"
+  },
+  "newProjectRoot": "projects",
+  "projects": {
+    "Tik-tok": {
+      "projectType": "application",
+      "schematics": {},
+      "root": "",
+      "sourceRoot": "src",
+      "prefix": "app",
+      "architect": {
+        "build": {
+          "builder": "@angular/build:application",
+          "options": {
+            "browser": "src/main.ts",
+            "tsConfig": "tsconfig.app.json",
+            "assets": [
+              {
+                "glob": "**/*",
+                "input": "public"
+              }
+            ],
+            "styles": [
+              "src/styles.css"
+            ]
+          },
+          "configurations": {
+            "production": {
+              "budgets": [
+                {
+                  "type": "initial",
+                  "maximumWarning": "1.5MB",
+                  "maximumError": "2MB"
+                },
+                {
+                  "type": "anyComponentStyle",
+                  "maximumWarning": "150kB",
+                  "maximumError": "200kB"
+                }
+              ],
+              "outputHashing": "all",
+              "fileReplacements": [
+                {
+                  "replace": "src/environments/environment.ts",
+                  "with": "src/environments/environment.prod.ts"
+                }
+              ]
+            },
+            "development": {
+              "optimization": false,
+              "extractLicenses": false,
+              "sourceMap": true
+            }
+          },
+          "defaultConfiguration": "production"
+        },
+        "serve": {
+          "builder": "@angular/build:dev-server",
+          "configurations": {
+            "production": {
+              "buildTarget": "Tik-tok:build:production"
+            },
+            "development": {
+              "buildTarget": "Tik-tok:build:development"
+            }
+          },
+          "defaultConfiguration": "development"
+        },
+        "test": {
+          "builder": "@angular/build:unit-test"
+        }
+      }
+    }
+  }
+}

+ 1 - 0
body.json

@@ -0,0 +1 @@
+{"choices":[{"finish_reason":"stop","index":0,"logprobs":null,"message":{"annotations":[],"content":"Pong! How can I assist you today?","refusal":null,"role":"assistant"}}],"created":1777349287,"id":"chatcmpl-DZU1vQejM8OeQtYhwIR1iIMabgtHB","model":"gpt-4o-mini-2024-07-18","object":"chat.completion","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":{"completion_tokens":11,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":8,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":19}}

+ 9356 - 0
package-lock.json

@@ -0,0 +1,9356 @@
+{
+  "name": "tik-tok",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "tik-tok",
+      "version": "0.0.0",
+      "dependencies": {
+        "@angular/common": "^21.2.0",
+        "@angular/compiler": "^21.2.0",
+        "@angular/core": "^21.2.0",
+        "@angular/forms": "^21.2.0",
+        "@angular/platform-browser": "^21.2.0",
+        "@angular/router": "^21.2.0",
+        "axios": "^1.15.0",
+        "cors": "^2.8.5",
+        "express": "^4.21.0",
+        "multer": "^2.1.1",
+        "rxjs": "~7.8.0",
+        "tslib": "^2.3.0"
+      },
+      "devDependencies": {
+        "@angular/build": "^21.2.3",
+        "@angular/cli": "^21.2.3",
+        "@angular/compiler-cli": "^21.2.0",
+        "concurrently": "^9.1.0",
+        "jsdom": "^28.0.0",
+        "prettier": "^3.8.1",
+        "typescript": "~5.9.2",
+        "vitest": "^4.0.8"
+      }
+    },
+    "node_modules/@acemir/cssom": {
+      "version": "0.9.31",
+      "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz",
+      "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@algolia/abtesting": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz",
+      "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-abtesting": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz",
+      "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-analytics": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz",
+      "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-common": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz",
+      "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-insights": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz",
+      "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-personalization": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz",
+      "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-query-suggestions": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz",
+      "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/client-search": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz",
+      "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/ingestion": {
+      "version": "1.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz",
+      "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/monitoring": {
+      "version": "1.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz",
+      "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/recommend": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz",
+      "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/requester-browser-xhr": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz",
+      "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/requester-fetch": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz",
+      "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@algolia/requester-node-http": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz",
+      "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/client-common": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/@ampproject/remapping": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@angular-devkit/architect": {
+      "version": "0.2102.7",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.7.tgz",
+      "integrity": "sha512-4K/5hln9iaPEt3F/NyYqncNLvYpzSjRslEkHl2xIgZwQsIFHEvhnDRBYj2/oatURQhBqO/Yu15z/icVOYLxuTg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@angular-devkit/core": "21.2.7",
+        "rxjs": "7.8.2"
+      },
+      "bin": {
+        "architect": "bin/cli.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular-devkit/core": {
+      "version": "21.2.7",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.7.tgz",
+      "integrity": "sha512-DONYY5u4IENO2qpd23mODaE4JI2EIohWV1kuJnsU9HIcm5wN714QB2z9WY/s4gLfUiAMIUu/8lpnW/0kOQZAnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "8.18.0",
+        "ajv-formats": "3.0.1",
+        "jsonc-parser": "3.3.1",
+        "picomatch": "4.0.4",
+        "rxjs": "7.8.2",
+        "source-map": "0.7.6"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "peerDependencies": {
+        "chokidar": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "chokidar": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular-devkit/schematics": {
+      "version": "21.2.7",
+      "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.7.tgz",
+      "integrity": "sha512-LYAjjUI1qM7pR/sd0yYt8OLA6ljOOXjcfzV40I5XQNmhAxq90YYS5xwMcixOmWX+z5zvCYGvPXvJGWjzio6SUg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@angular-devkit/core": "21.2.7",
+        "jsonc-parser": "3.3.1",
+        "magic-string": "0.30.21",
+        "ora": "9.3.0",
+        "rxjs": "7.8.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular/build": {
+      "version": "21.2.7",
+      "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.7.tgz",
+      "integrity": "sha512-FpSkFqpsJtdN1cROekVYkmeV1QepdP+/d7fyYQEuNmlOlyqXSDh9qJmy4iL9VNbAU0rk+vFCtYM86rO7Pt9cSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ampproject/remapping": "2.3.0",
+        "@angular-devkit/architect": "0.2102.7",
+        "@babel/core": "7.29.0",
+        "@babel/helper-annotate-as-pure": "7.27.3",
+        "@babel/helper-split-export-declaration": "7.24.7",
+        "@inquirer/confirm": "5.1.21",
+        "@vitejs/plugin-basic-ssl": "2.1.4",
+        "beasties": "0.4.1",
+        "browserslist": "^4.26.0",
+        "esbuild": "0.27.3",
+        "https-proxy-agent": "7.0.6",
+        "istanbul-lib-instrument": "6.0.3",
+        "jsonc-parser": "3.3.1",
+        "listr2": "9.0.5",
+        "magic-string": "0.30.21",
+        "mrmime": "2.0.1",
+        "parse5-html-rewriting-stream": "8.0.0",
+        "picomatch": "4.0.4",
+        "piscina": "5.1.4",
+        "rolldown": "1.0.0-rc.4",
+        "sass": "1.97.3",
+        "semver": "7.7.4",
+        "source-map-support": "0.5.21",
+        "tinyglobby": "0.2.15",
+        "undici": "7.24.4",
+        "vite": "7.3.2",
+        "watchpack": "2.5.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      },
+      "optionalDependencies": {
+        "lmdb": "3.5.1"
+      },
+      "peerDependencies": {
+        "@angular/compiler": "^21.0.0",
+        "@angular/compiler-cli": "^21.0.0",
+        "@angular/core": "^21.0.0",
+        "@angular/localize": "^21.0.0",
+        "@angular/platform-browser": "^21.0.0",
+        "@angular/platform-server": "^21.0.0",
+        "@angular/service-worker": "^21.0.0",
+        "@angular/ssr": "^21.2.7",
+        "karma": "^6.4.0",
+        "less": "^4.2.0",
+        "ng-packagr": "^21.0.0",
+        "postcss": "^8.4.0",
+        "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0",
+        "tslib": "^2.3.0",
+        "typescript": ">=5.9 <6.0",
+        "vitest": "^4.0.8"
+      },
+      "peerDependenciesMeta": {
+        "@angular/core": {
+          "optional": true
+        },
+        "@angular/localize": {
+          "optional": true
+        },
+        "@angular/platform-browser": {
+          "optional": true
+        },
+        "@angular/platform-server": {
+          "optional": true
+        },
+        "@angular/service-worker": {
+          "optional": true
+        },
+        "@angular/ssr": {
+          "optional": true
+        },
+        "karma": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "ng-packagr": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        },
+        "tailwindcss": {
+          "optional": true
+        },
+        "vitest": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular/cli": {
+      "version": "21.2.7",
+      "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.7.tgz",
+      "integrity": "sha512-N/wj8fFRB718efIFYpwnYfy+MecZREZXsUNMTVndFLH6T0jCheb9PVetR6jsyZp6h46USNPOmJYJ/9255lME+Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@angular-devkit/architect": "0.2102.7",
+        "@angular-devkit/core": "21.2.7",
+        "@angular-devkit/schematics": "21.2.7",
+        "@inquirer/prompts": "7.10.1",
+        "@listr2/prompt-adapter-inquirer": "3.0.5",
+        "@modelcontextprotocol/sdk": "1.26.0",
+        "@schematics/angular": "21.2.7",
+        "@yarnpkg/lockfile": "1.1.0",
+        "algoliasearch": "5.48.1",
+        "ini": "6.0.0",
+        "jsonc-parser": "3.3.1",
+        "listr2": "9.0.5",
+        "npm-package-arg": "13.0.2",
+        "pacote": "21.3.1",
+        "parse5-html-rewriting-stream": "8.0.0",
+        "semver": "7.7.4",
+        "yargs": "18.0.0",
+        "zod": "4.3.6"
+      },
+      "bin": {
+        "ng": "bin/ng.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@angular/common": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.8.tgz",
+      "integrity": "sha512-ZvgcxsLPkSG0B1jc2ZXshAWIFBoQ0U9uwIX/zG/RGcfMpoKyEDNAebli6FTIpxIlz/35rtBNV7EGPhinjPTJFQ==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/core": "21.2.8",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/compiler": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.8.tgz",
+      "integrity": "sha512-Il9KlT6qX8rWmun5jY6wMLx56bCQZpOVIFEyHM4ai2wmxvbqyxgRFKDs4iMRNn1h04Tgupl6cKSqP9lecIvH6w==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@angular/compiler-cli": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.8.tgz",
+      "integrity": "sha512-S0W+6QazCsn/4xWZu0V5VmU9zmKIlqFR2FJSsAQUPReVmpA40SuQSP6A/cyMVIMYaHvO/cAXSHJVgpxBzBSL/Q==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/core": "7.29.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14",
+        "chokidar": "^5.0.0",
+        "convert-source-map": "^1.5.1",
+        "reflect-metadata": "^0.2.0",
+        "semver": "^7.0.0",
+        "tslib": "^2.3.0",
+        "yargs": "^18.0.0"
+      },
+      "bin": {
+        "ng-xi18n": "bundles/src/bin/ng_xi18n.js",
+        "ngc": "bundles/src/bin/ngc.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler": "21.2.8",
+        "typescript": ">=5.9 <6.1"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular/core": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.8.tgz",
+      "integrity": "sha512-hI7n4t8qgFJaVV55LIaNuzcdP+/IeuqQRu3huSLo47Gf6uZAD0Acj4Ye9SC8YNmhUu5/RiImngm9NOlcI2oCJA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler": "21.2.8",
+        "rxjs": "^6.5.3 || ^7.4.0",
+        "zone.js": "~0.15.0 || ~0.16.0"
+      },
+      "peerDependenciesMeta": {
+        "@angular/compiler": {
+          "optional": true
+        },
+        "zone.js": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular/forms": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.8.tgz",
+      "integrity": "sha512-tyQAHjfMHcqETRkKQaZHjYqIK9W8uRenPpY2DF/Jl+S7CwcaX4T8t8TKgzvTynNzQW9QGiLg0pqVosVMKzBXJg==",
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.0.0",
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "21.2.8",
+        "@angular/core": "21.2.8",
+        "@angular/platform-browser": "21.2.8",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/platform-browser": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.8.tgz",
+      "integrity": "sha512-4fwmGf7GCuIsjFqx1gqqWC92YjlN9SmGJO17TPPsOm5zUOnDx+h3Bj9XjdXxlcBtugTb2xHk6Auqyv3lzWGlkw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/animations": "21.2.8",
+        "@angular/common": "21.2.8",
+        "@angular/core": "21.2.8"
+      },
+      "peerDependenciesMeta": {
+        "@angular/animations": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular/router": {
+      "version": "21.2.8",
+      "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.8.tgz",
+      "integrity": "sha512-KSlUbFHHKY84G6iKlB2FDMmh+lLmGjmpyT1p/kx8qZm1BuxJGOOU+oNgkCfaPJT1R2/muDXuxQ51uc/la6y28g==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "21.2.8",
+        "@angular/core": "21.2.8",
+        "@angular/platform-browser": "21.2.8",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@asamuzakjp/css-color": {
+      "version": "5.1.9",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.9.tgz",
+      "integrity": "sha512-zd9c/Wdso6v1U7v6w3i/hbAr4K7NaSHImdpvmLt+Y9ea5BhilnIGNkfhOJ7FEIuPipAnE9tZeDOll05WDT0kgg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/css-calc": "^3.1.1",
+        "@csstools/css-color-parser": "^4.0.2",
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/@asamuzakjp/dom-selector": {
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz",
+      "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/nwsapi": "^2.3.9",
+        "bidi-js": "^1.0.3",
+        "css-tree": "^3.1.0",
+        "is-potential-custom-element-name": "^1.0.1",
+        "lru-cache": "^11.2.6"
+      }
+    },
+    "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@asamuzakjp/nwsapi": {
+      "version": "2.3.9",
+      "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+      "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/core/node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-annotate-as-pure": {
+      "version": "7.27.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+      "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.27.3"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-split-export-declaration": {
+      "version": "7.24.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
+      "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.24.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+      "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+      "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@bramus/specificity": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+      "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "css-tree": "^3.0.0"
+      },
+      "bin": {
+        "specificity": "bin/cli.js"
+      }
+    },
+    "node_modules/@csstools/color-helpers": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
+      "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/@csstools/css-calc": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
+      "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-color-parser": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
+      "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@csstools/color-helpers": "^6.0.2",
+        "@csstools/css-calc": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-parser-algorithms": "^4.0.0",
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-parser-algorithms": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+      "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "peerDependencies": {
+        "@csstools/css-tokenizer": "^4.0.0"
+      }
+    },
+    "node_modules/@csstools/css-syntax-patches-for-csstree": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz",
+      "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT-0",
+      "peerDependencies": {
+        "css-tree": "^3.2.1"
+      },
+      "peerDependenciesMeta": {
+        "css-tree": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@csstools/css-tokenizer": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+      "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/csstools"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/csstools"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=20.19.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+      "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+      "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+      "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+      "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+      "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+      "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+      "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+      "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+      "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+      "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+      "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+      "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+      "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+      "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+      "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+      "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+      "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+      "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+      "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+      "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+      "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+      "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+      "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+      "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
+      "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@exodus/bytes": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
+      "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@noble/hashes": "^1.8.0 || ^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@noble/hashes": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@gar/promise-retry": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz",
+      "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@harperfast/extended-iterable": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz",
+      "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true
+    },
+    "node_modules/@hono/node-server": {
+      "version": "1.19.13",
+      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
+      "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.14.1"
+      },
+      "peerDependencies": {
+        "hono": "^4"
+      }
+    },
+    "node_modules/@inquirer/ansi": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
+      "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@inquirer/checkbox": {
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
+      "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/ansi": "^1.0.2",
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/figures": "^1.0.15",
+        "@inquirer/type": "^3.0.10",
+        "yoctocolors-cjs": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/confirm": {
+      "version": "5.1.21",
+      "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
+      "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/type": "^3.0.10"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/core": {
+      "version": "10.3.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
+      "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/ansi": "^1.0.2",
+        "@inquirer/figures": "^1.0.15",
+        "@inquirer/type": "^3.0.10",
+        "cli-width": "^4.1.0",
+        "mute-stream": "^2.0.0",
+        "signal-exit": "^4.1.0",
+        "wrap-ansi": "^6.2.0",
+        "yoctocolors-cjs": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/editor": {
+      "version": "4.2.23",
+      "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
+      "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/external-editor": "^1.0.3",
+        "@inquirer/type": "^3.0.10"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/expand": {
+      "version": "4.0.23",
+      "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
+      "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/type": "^3.0.10",
+        "yoctocolors-cjs": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/external-editor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
+      "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chardet": "^2.1.1",
+        "iconv-lite": "^0.7.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/figures": {
+      "version": "1.0.15",
+      "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
+      "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@inquirer/input": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
+      "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/type": "^3.0.10"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/number": {
+      "version": "3.0.23",
+      "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
+      "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/type": "^3.0.10"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/password": {
+      "version": "4.0.23",
+      "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
+      "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/ansi": "^1.0.2",
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/type": "^3.0.10"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/prompts": {
+      "version": "7.10.1",
+      "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz",
+      "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@inquirer/checkbox": "^4.3.2",
+        "@inquirer/confirm": "^5.1.21",
+        "@inquirer/editor": "^4.2.23",
+        "@inquirer/expand": "^4.0.23",
+        "@inquirer/input": "^4.3.1",
+        "@inquirer/number": "^3.0.23",
+        "@inquirer/password": "^4.0.23",
+        "@inquirer/rawlist": "^4.1.11",
+        "@inquirer/search": "^3.2.2",
+        "@inquirer/select": "^4.4.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/rawlist": {
+      "version": "4.1.11",
+      "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
+      "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/type": "^3.0.10",
+        "yoctocolors-cjs": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/search": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
+      "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/figures": "^1.0.15",
+        "@inquirer/type": "^3.0.10",
+        "yoctocolors-cjs": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/select": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
+      "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/ansi": "^1.0.2",
+        "@inquirer/core": "^10.3.2",
+        "@inquirer/figures": "^1.0.15",
+        "@inquirer/type": "^3.0.10",
+        "yoctocolors-cjs": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@inquirer/type": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
+      "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@types/node": ">=18"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@isaacs/fs-minipass": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+      "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.4"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/@istanbuljs/schema": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+      "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@listr2/prompt-adapter-inquirer": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz",
+      "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@inquirer/type": "^3.0.8"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      },
+      "peerDependencies": {
+        "@inquirer/prompts": ">= 3 < 8",
+        "listr2": "9.0.5"
+      }
+    },
+    "node_modules/@lmdb/lmdb-darwin-arm64": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz",
+      "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-darwin-x64": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz",
+      "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-linux-arm": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz",
+      "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-linux-arm64": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz",
+      "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-linux-x64": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz",
+      "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-win32-arm64": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz",
+      "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@lmdb/lmdb-win32-x64": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz",
+      "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@modelcontextprotocol/sdk": {
+      "version": "1.26.0",
+      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
+      "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@hono/node-server": "^1.19.9",
+        "ajv": "^8.17.1",
+        "ajv-formats": "^3.0.1",
+        "content-type": "^1.0.5",
+        "cors": "^2.8.5",
+        "cross-spawn": "^7.0.5",
+        "eventsource": "^3.0.2",
+        "eventsource-parser": "^3.0.0",
+        "express": "^5.2.1",
+        "express-rate-limit": "^8.2.1",
+        "hono": "^4.11.4",
+        "jose": "^6.1.3",
+        "json-schema-typed": "^8.0.2",
+        "pkce-challenge": "^5.0.0",
+        "raw-body": "^3.0.0",
+        "zod": "^3.25 || ^4.0",
+        "zod-to-json-schema": "^3.25.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "@cfworker/json-schema": "^4.1.1",
+        "zod": "^3.25 || ^4.0"
+      },
+      "peerDependenciesMeta": {
+        "@cfworker/json-schema": {
+          "optional": true
+        },
+        "zod": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+      "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.3",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.7.0",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.1",
+        "raw-body": "^3.0.1",
+        "type-is": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+      "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/express": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+      "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.1",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+      "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.3",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.1",
+        "mime-types": "^3.0.2",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+      "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
+      "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
+      "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
+      "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
+      "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
+      "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
+      "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@napi-rs/nice": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz",
+      "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">= 10"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "optionalDependencies": {
+        "@napi-rs/nice-android-arm-eabi": "1.1.1",
+        "@napi-rs/nice-android-arm64": "1.1.1",
+        "@napi-rs/nice-darwin-arm64": "1.1.1",
+        "@napi-rs/nice-darwin-x64": "1.1.1",
+        "@napi-rs/nice-freebsd-x64": "1.1.1",
+        "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1",
+        "@napi-rs/nice-linux-arm64-gnu": "1.1.1",
+        "@napi-rs/nice-linux-arm64-musl": "1.1.1",
+        "@napi-rs/nice-linux-ppc64-gnu": "1.1.1",
+        "@napi-rs/nice-linux-riscv64-gnu": "1.1.1",
+        "@napi-rs/nice-linux-s390x-gnu": "1.1.1",
+        "@napi-rs/nice-linux-x64-gnu": "1.1.1",
+        "@napi-rs/nice-linux-x64-musl": "1.1.1",
+        "@napi-rs/nice-openharmony-arm64": "1.1.1",
+        "@napi-rs/nice-win32-arm64-msvc": "1.1.1",
+        "@napi-rs/nice-win32-ia32-msvc": "1.1.1",
+        "@napi-rs/nice-win32-x64-msvc": "1.1.1"
+      }
+    },
+    "node_modules/@napi-rs/nice-android-arm-eabi": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz",
+      "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-android-arm64": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz",
+      "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-darwin-arm64": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz",
+      "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-darwin-x64": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz",
+      "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-freebsd-x64": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz",
+      "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-arm-gnueabihf": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz",
+      "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-arm64-gnu": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz",
+      "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-arm64-musl": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz",
+      "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-ppc64-gnu": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz",
+      "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-riscv64-gnu": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz",
+      "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-s390x-gnu": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz",
+      "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-x64-gnu": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz",
+      "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-linux-x64-musl": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz",
+      "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-openharmony-arm64": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz",
+      "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-win32-arm64-msvc": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz",
+      "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-win32-ia32-msvc": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz",
+      "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/nice-win32-x64-msvc": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz",
+      "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
+      "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.1"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
+      }
+    },
+    "node_modules/@npmcli/agent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz",
+      "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.1",
+        "lru-cache": "^11.2.1",
+        "socks-proxy-agent": "^8.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/agent/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@npmcli/fs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz",
+      "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/git": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz",
+      "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@npmcli/promise-spawn": "^9.0.0",
+        "ini": "^6.0.0",
+        "lru-cache": "^11.2.1",
+        "npm-pick-manifest": "^11.0.1",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "which": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/git/node_modules/isexe": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+      "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/@npmcli/git/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@npmcli/git/node_modules/which": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+      "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^4.0.0"
+      },
+      "bin": {
+        "node-which": "bin/which.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/installed-package-contents": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz",
+      "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-bundled": "^5.0.0",
+        "npm-normalize-package-bin": "^5.0.0"
+      },
+      "bin": {
+        "installed-package-contents": "bin/index.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/node-gyp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz",
+      "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/package-json": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz",
+      "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/git": "^7.0.0",
+        "glob": "^13.0.0",
+        "hosted-git-info": "^9.0.0",
+        "json-parse-even-better-errors": "^5.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.5.3",
+        "spdx-expression-parse": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/promise-spawn": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz",
+      "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "which": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/promise-spawn/node_modules/isexe": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+      "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/@npmcli/promise-spawn/node_modules/which": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+      "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^4.0.0"
+      },
+      "bin": {
+        "node-which": "bin/which.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/redact": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz",
+      "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@npmcli/run-script": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz",
+      "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/node-gyp": "^5.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "@npmcli/promise-spawn": "^9.0.0",
+        "node-gyp": "^12.1.0",
+        "proc-log": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.113.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz",
+      "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+      "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3",
+        "is-glob": "^4.0.3",
+        "node-addon-api": "^7.0.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.6",
+        "@parcel/watcher-darwin-arm64": "2.5.6",
+        "@parcel/watcher-darwin-x64": "2.5.6",
+        "@parcel/watcher-freebsd-x64": "2.5.6",
+        "@parcel/watcher-linux-arm-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm-musl": "2.5.6",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm64-musl": "2.5.6",
+        "@parcel/watcher-linux-x64-glibc": "2.5.6",
+        "@parcel/watcher-linux-x64-musl": "2.5.6",
+        "@parcel/watcher-win32-arm64": "2.5.6",
+        "@parcel/watcher-win32-ia32": "2.5.6",
+        "@parcel/watcher-win32-x64": "2.5.6"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+      "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+      "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+      "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+      "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+      "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+      "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+      "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+      "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+      "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+      "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+      "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+      "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+      "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher/node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz",
+      "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz",
+      "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz",
+      "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz",
+      "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz",
+      "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz",
+      "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz",
+      "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz",
+      "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz",
+      "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz",
+      "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz",
+      "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@napi-rs/wasm-runtime": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz",
+      "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz",
+      "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz",
+      "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+      "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+      "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+      "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+      "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+      "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+      "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+      "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+      "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+      "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+      "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+      "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+      "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+      "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+      "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+      "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+      "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+      "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+      "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+      "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+      "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+      "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+      "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+      "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+      "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+      "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@schematics/angular": {
+      "version": "21.2.7",
+      "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.7.tgz",
+      "integrity": "sha512-aqEj3RyBtmH+41HZvrbfrpCo0e+0NzwyQyNSC/wLDShVqoidBtPbEdHU1FZ4+ni41da7rI3F12gUuAHws27kMA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@angular-devkit/core": "21.2.7",
+        "@angular-devkit/schematics": "21.2.7",
+        "jsonc-parser": "3.3.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
+        "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
+        "yarn": ">= 1.13.0"
+      }
+    },
+    "node_modules/@sigstore/bundle": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz",
+      "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/protobuf-specs": "^0.5.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@sigstore/core": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz",
+      "integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@sigstore/protobuf-specs": {
+      "version": "0.5.1",
+      "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz",
+      "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/@sigstore/sign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz",
+      "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.2",
+        "@sigstore/bundle": "^4.0.0",
+        "@sigstore/core": "^3.2.0",
+        "@sigstore/protobuf-specs": "^0.5.0",
+        "make-fetch-happen": "^15.0.4",
+        "proc-log": "^6.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@sigstore/tuf": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz",
+      "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/protobuf-specs": "^0.5.0",
+        "tuf-js": "^4.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@sigstore/verify": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz",
+      "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/bundle": "^4.0.0",
+        "@sigstore/core": "^3.1.0",
+        "@sigstore/protobuf-specs": "^0.5.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "license": "MIT"
+    },
+    "node_modules/@tufjs/canonical-json": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+      "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@tufjs/models": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz",
+      "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tufjs/canonical-json": "2.0.0",
+        "minimatch": "^10.1.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+      "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-basic-ssl": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz",
+      "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz",
+      "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.4",
+        "@vitest/utils": "4.1.4",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz",
+      "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.4",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
+      "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz",
+      "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.4",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz",
+      "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.4",
+        "@vitest/utils": "4.1.4",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz",
+      "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
+      "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.4",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils/node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@yarnpkg/lockfile": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+      "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/abbrev": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
+      "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accepts/node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+      "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+      "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/algoliasearch": {
+      "version": "5.48.1",
+      "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz",
+      "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@algolia/abtesting": "1.14.1",
+        "@algolia/client-abtesting": "5.48.1",
+        "@algolia/client-analytics": "5.48.1",
+        "@algolia/client-common": "5.48.1",
+        "@algolia/client-insights": "5.48.1",
+        "@algolia/client-personalization": "5.48.1",
+        "@algolia/client-query-suggestions": "5.48.1",
+        "@algolia/client-search": "5.48.1",
+        "@algolia/ingestion": "1.48.1",
+        "@algolia/monitoring": "1.48.1",
+        "@algolia/recommend": "5.48.1",
+        "@algolia/requester-browser-xhr": "5.48.1",
+        "@algolia/requester-fetch": "5.48.1",
+        "@algolia/requester-node-http": "5.48.1"
+      },
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
+      "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "environment": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
+      "license": "MIT"
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.15.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
+      "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.10.17",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz",
+      "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/beasties": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz",
+      "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "css-select": "^6.0.0",
+        "css-what": "^7.0.0",
+        "dom-serializer": "^2.0.0",
+        "domhandler": "^5.0.3",
+        "htmlparser2": "^10.0.0",
+        "picocolors": "^1.1.1",
+        "postcss": "^8.4.49",
+        "postcss-media-query-parser": "^0.2.3",
+        "postcss-safe-parser": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/bidi-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+      "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "require-from-string": "^2.0.2"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.4",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+      "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.14.0",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/body-parser/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/body-parser/node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/body-parser/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser/node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/brace-expansion": {
+      "version": "5.0.5",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+      "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+      "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.12",
+        "caniuse-lite": "^1.0.30001782",
+        "electron-to-chromium": "^1.5.328",
+        "node-releases": "^2.0.36",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "license": "MIT"
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/cacache": {
+      "version": "20.0.4",
+      "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz",
+      "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/fs": "^5.0.0",
+        "fs-minipass": "^3.0.0",
+        "glob": "^13.0.0",
+        "lru-cache": "^11.1.0",
+        "minipass": "^7.0.3",
+        "minipass-collect": "^2.0.1",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "p-map": "^7.0.2",
+        "ssri": "^13.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/cacache/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001787",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+      "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chardet": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
+      "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+      "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/cli-cursor": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+      "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "restore-cursor": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-spinners": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
+      "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-truncate": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
+      "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "slice-ansi": "^8.0.0",
+        "string-width": "^8.2.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-width": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+      "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+      "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^7.2.0",
+        "strip-ansi": "^7.1.0",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cliui/node_modules/wrap-ansi": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+      "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/concat-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+      "engines": [
+        "node >= 6.0"
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.0.2",
+        "typedarray": "^0.0.6"
+      }
+    },
+    "node_modules/concurrently": {
+      "version": "9.2.1",
+      "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+      "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "4.1.2",
+        "rxjs": "7.8.2",
+        "shell-quote": "1.8.3",
+        "supports-color": "8.1.1",
+        "tree-kill": "1.2.2",
+        "yargs": "17.7.2"
+      },
+      "bin": {
+        "conc": "dist/bin/concurrently.js",
+        "concurrently": "dist/bin/concurrently.js"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+      }
+    },
+    "node_modules/concurrently/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/concurrently/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/concurrently/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/concurrently/node_modules/cliui": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.1",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/concurrently/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/concurrently/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/concurrently/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/concurrently/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/concurrently/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/concurrently/node_modules/yargs": {
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^8.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.3",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^21.1.1"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/concurrently/node_modules/yargs-parser": {
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-select": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz",
+      "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0",
+        "css-what": "^7.0.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.2.2",
+        "nth-check": "^2.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/css-tree": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+      "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mdn-data": "2.27.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+      }
+    },
+    "node_modules/css-what": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz",
+      "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">= 6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/cssstyle": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz",
+      "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@asamuzakjp/css-color": "^5.0.1",
+        "@csstools/css-syntax-patches-for-csstree": "^1.0.28",
+        "css-tree": "^3.1.0",
+        "lru-cache": "^11.2.6"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/cssstyle/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/data-urls": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+      "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^16.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+      "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dom-serializer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+      "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.2",
+        "entities": "^4.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+      "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "^2.3.0"
+      },
+      "engines": {
+        "node": ">= 4"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domhandler?sponsor=1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+      "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "^2.0.0",
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domutils?sponsor=1"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.334",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz",
+      "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/environment": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+      "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/err-code": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+      "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+      "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.27.3",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
+      "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.27.3",
+        "@esbuild/android-arm": "0.27.3",
+        "@esbuild/android-arm64": "0.27.3",
+        "@esbuild/android-x64": "0.27.3",
+        "@esbuild/darwin-arm64": "0.27.3",
+        "@esbuild/darwin-x64": "0.27.3",
+        "@esbuild/freebsd-arm64": "0.27.3",
+        "@esbuild/freebsd-x64": "0.27.3",
+        "@esbuild/linux-arm": "0.27.3",
+        "@esbuild/linux-arm64": "0.27.3",
+        "@esbuild/linux-ia32": "0.27.3",
+        "@esbuild/linux-loong64": "0.27.3",
+        "@esbuild/linux-mips64el": "0.27.3",
+        "@esbuild/linux-ppc64": "0.27.3",
+        "@esbuild/linux-riscv64": "0.27.3",
+        "@esbuild/linux-s390x": "0.27.3",
+        "@esbuild/linux-x64": "0.27.3",
+        "@esbuild/netbsd-arm64": "0.27.3",
+        "@esbuild/netbsd-x64": "0.27.3",
+        "@esbuild/openbsd-arm64": "0.27.3",
+        "@esbuild/openbsd-x64": "0.27.3",
+        "@esbuild/openharmony-arm64": "0.27.3",
+        "@esbuild/sunos-x64": "0.27.3",
+        "@esbuild/win32-arm64": "0.27.3",
+        "@esbuild/win32-ia32": "0.27.3",
+        "@esbuild/win32-x64": "0.27.3"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+      "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/eventsource": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
+      "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eventsource-parser": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/eventsource-parser": {
+      "version": "3.0.6",
+      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
+      "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/expect-type": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+      "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/exponential-backoff": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
+      "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/express": {
+      "version": "4.22.1",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+      "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.3",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.14.0",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/express-rate-limit": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
+      "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "10.1.0"
+      },
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/express-rate-limit"
+      },
+      "peerDependencies": {
+        "express": ">= 4.11"
+      }
+    },
+    "node_modules/express/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/express/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+      "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/finalhandler/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/finalhandler/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+      "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.3"
+      },
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz",
+      "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "13.0.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+      "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "minimatch": "^10.2.2",
+        "minipass": "^7.1.3",
+        "path-scurry": "^2.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-to-regexp": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+      "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hono": {
+      "version": "4.12.12",
+      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
+      "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "engines": {
+        "node": ">=16.9.0"
+      }
+    },
+    "node_modules/hosted-git-info": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
+      "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^11.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/hosted-git-info/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/html-encoding-sniffer": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+      "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.6.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/htmlparser2": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+      "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+      "dev": true,
+      "funding": [
+        "https://github.com/fb55/htmlparser2?sponsor=1",
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.3.0",
+        "domhandler": "^5.0.3",
+        "domutils": "^3.2.2",
+        "entities": "^7.0.1"
+      }
+    },
+    "node_modules/htmlparser2/node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/http-cache-semantics": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+      "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+      "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+      "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/ignore-walk": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz",
+      "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minimatch": "^10.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/immutable": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
+      "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
+      "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/ip-address": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+      "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
+      "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.3.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-interactive": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+      "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-potential-custom-element-name": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+      "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/istanbul-lib-coverage": {
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/istanbul-lib-instrument": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+      "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/core": "^7.23.9",
+        "@babel/parser": "^7.23.9",
+        "@istanbuljs/schema": "^0.1.3",
+        "istanbul-lib-coverage": "^3.2.0",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/jose": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
+      "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/panva"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsdom": {
+      "version": "28.1.0",
+      "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz",
+      "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@acemir/cssom": "^0.9.31",
+        "@asamuzakjp/dom-selector": "^6.8.1",
+        "@bramus/specificity": "^2.4.2",
+        "@exodus/bytes": "^1.11.0",
+        "cssstyle": "^6.0.1",
+        "data-urls": "^7.0.0",
+        "decimal.js": "^10.6.0",
+        "html-encoding-sniffer": "^6.0.0",
+        "http-proxy-agent": "^7.0.2",
+        "https-proxy-agent": "^7.0.6",
+        "is-potential-custom-element-name": "^1.0.1",
+        "parse5": "^8.0.0",
+        "saxes": "^6.0.0",
+        "symbol-tree": "^3.2.4",
+        "tough-cookie": "^6.0.0",
+        "undici": "^7.21.0",
+        "w3c-xmlserializer": "^5.0.0",
+        "webidl-conversions": "^8.0.1",
+        "whatwg-mimetype": "^5.0.0",
+        "whatwg-url": "^16.0.0",
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "canvas": "^3.0.0"
+      },
+      "peerDependenciesMeta": {
+        "canvas": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
+      "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-typed": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
+      "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jsonc-parser": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
+      "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsonparse": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+      "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+      "dev": true,
+      "engines": [
+        "node >= 0.2.0"
+      ],
+      "license": "MIT"
+    },
+    "node_modules/listr2": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
+      "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "cli-truncate": "^5.0.0",
+        "colorette": "^2.0.20",
+        "eventemitter3": "^5.0.1",
+        "log-update": "^6.1.0",
+        "rfdc": "^1.4.1",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/listr2/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/listr2/node_modules/wrap-ansi": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+      "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/lmdb": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz",
+      "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@harperfast/extended-iterable": "^1.0.3",
+        "msgpackr": "^1.11.2",
+        "node-addon-api": "^6.1.0",
+        "node-gyp-build-optional-packages": "5.2.2",
+        "ordered-binary": "^1.5.3",
+        "weak-lru-cache": "^1.2.2"
+      },
+      "bin": {
+        "download-lmdb-prebuilds": "bin/download-prebuilds.js"
+      },
+      "optionalDependencies": {
+        "@lmdb/lmdb-darwin-arm64": "3.5.1",
+        "@lmdb/lmdb-darwin-x64": "3.5.1",
+        "@lmdb/lmdb-linux-arm": "3.5.1",
+        "@lmdb/lmdb-linux-arm64": "3.5.1",
+        "@lmdb/lmdb-linux-x64": "3.5.1",
+        "@lmdb/lmdb-win32-arm64": "3.5.1",
+        "@lmdb/lmdb-win32-x64": "3.5.1"
+      }
+    },
+    "node_modules/log-symbols": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz",
+      "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-unicode-supported": "^2.0.0",
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-update": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
+      "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^7.0.0",
+        "cli-cursor": "^5.0.0",
+        "slice-ansi": "^7.1.0",
+        "strip-ansi": "^7.1.0",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-update/node_modules/slice-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
+      "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "is-fullwidth-code-point": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/log-update/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/log-update/node_modules/wrap-ansi": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+      "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/make-fetch-happen": {
+      "version": "15.0.5",
+      "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz",
+      "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@npmcli/agent": "^4.0.0",
+        "@npmcli/redact": "^4.0.0",
+        "cacache": "^20.0.1",
+        "http-cache-semantics": "^4.1.1",
+        "minipass": "^7.0.2",
+        "minipass-fetch": "^5.0.0",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "negotiator": "^1.0.0",
+        "proc-log": "^6.0.0",
+        "ssri": "^13.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mdn-data": {
+      "version": "2.27.1",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+      "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mimic-function": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+      "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+      "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/minipass-collect": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
+      "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.3"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/minipass-fetch": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz",
+      "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^7.0.3",
+        "minipass-sized": "^2.0.0",
+        "minizlib": "^3.0.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      },
+      "optionalDependencies": {
+        "iconv-lite": "^0.7.2"
+      }
+    },
+    "node_modules/minipass-flush": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz",
+      "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minipass-flush/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minipass-flush/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/minipass-pipeline": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+      "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minipass-pipeline/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minipass-pipeline/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/minipass-sized": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz",
+      "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+      "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/mrmime": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+      "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/msgpackr": {
+      "version": "1.11.9",
+      "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz",
+      "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "optionalDependencies": {
+        "msgpackr-extract": "^3.0.2"
+      }
+    },
+    "node_modules/msgpackr-extract": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
+      "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "node-gyp-build-optional-packages": "5.2.2"
+      },
+      "bin": {
+        "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
+      },
+      "optionalDependencies": {
+        "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
+        "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
+      }
+    },
+    "node_modules/multer": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
+      "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
+      "license": "MIT",
+      "dependencies": {
+        "append-field": "^1.0.0",
+        "busboy": "^1.6.0",
+        "concat-stream": "^2.0.0",
+        "type-is": "^1.6.18"
+      },
+      "engines": {
+        "node": ">= 10.16.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/mute-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+      "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+      "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/node-gyp": {
+      "version": "12.2.0",
+      "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz",
+      "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "env-paths": "^2.2.0",
+        "exponential-backoff": "^3.1.1",
+        "graceful-fs": "^4.2.6",
+        "make-fetch-happen": "^15.0.0",
+        "nopt": "^9.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "tar": "^7.5.4",
+        "tinyglobby": "^0.2.12",
+        "which": "^6.0.0"
+      },
+      "bin": {
+        "node-gyp": "bin/node-gyp.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/node-gyp-build-optional-packages": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
+      "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.1"
+      },
+      "bin": {
+        "node-gyp-build-optional-packages": "bin.js",
+        "node-gyp-build-optional-packages-optional": "optional.js",
+        "node-gyp-build-optional-packages-test": "build-test.js"
+      }
+    },
+    "node_modules/node-gyp/node_modules/isexe": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
+      "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/node-gyp/node_modules/which": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
+      "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^4.0.0"
+      },
+      "bin": {
+        "node-which": "bin/which.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.37",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+      "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nopt": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
+      "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "abbrev": "^4.0.0"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-bundled": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz",
+      "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-normalize-package-bin": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-install-checks": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz",
+      "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "semver": "^7.1.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-normalize-package-bin": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz",
+      "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-package-arg": {
+      "version": "13.0.2",
+      "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz",
+      "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "hosted-git-info": "^9.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "validate-npm-package-name": "^7.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-packlist": {
+      "version": "10.0.4",
+      "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz",
+      "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "ignore-walk": "^8.0.0",
+        "proc-log": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-pick-manifest": {
+      "version": "11.0.3",
+      "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz",
+      "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-install-checks": "^8.0.0",
+        "npm-normalize-package-bin": "^5.0.0",
+        "npm-package-arg": "^13.0.0",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-registry-fetch": {
+      "version": "19.1.1",
+      "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz",
+      "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/redact": "^4.0.0",
+        "jsonparse": "^1.3.1",
+        "make-fetch-happen": "^15.0.0",
+        "minipass": "^7.0.2",
+        "minipass-fetch": "^5.0.0",
+        "minizlib": "^3.0.1",
+        "npm-package-arg": "^13.0.0",
+        "proc-log": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/obug": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+      "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/sxzz",
+        "https://opencollective.com/debug"
+      ],
+      "license": "MIT"
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+      "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-function": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ora": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz",
+      "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^5.6.2",
+        "cli-cursor": "^5.0.0",
+        "cli-spinners": "^3.2.0",
+        "is-interactive": "^2.0.0",
+        "is-unicode-supported": "^2.1.0",
+        "log-symbols": "^7.0.1",
+        "stdin-discarder": "^0.3.1",
+        "string-width": "^8.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ordered-binary": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz",
+      "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/p-map": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
+      "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pacote": {
+      "version": "21.3.1",
+      "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz",
+      "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/git": "^7.0.0",
+        "@npmcli/installed-package-contents": "^4.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "@npmcli/promise-spawn": "^9.0.0",
+        "@npmcli/run-script": "^10.0.0",
+        "cacache": "^20.0.0",
+        "fs-minipass": "^3.0.0",
+        "minipass": "^7.0.2",
+        "npm-package-arg": "^13.0.0",
+        "npm-packlist": "^10.0.1",
+        "npm-pick-manifest": "^11.0.1",
+        "npm-registry-fetch": "^19.0.0",
+        "proc-log": "^6.0.0",
+        "promise-retry": "^2.0.1",
+        "sigstore": "^4.0.0",
+        "ssri": "^13.0.0",
+        "tar": "^7.4.3"
+      },
+      "bin": {
+        "pacote": "bin/index.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
+      "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^6.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5-html-rewriting-stream": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz",
+      "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "entities": "^6.0.0",
+        "parse5": "^8.0.0",
+        "parse5-sax-parser": "^8.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5-html-rewriting-stream/node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/parse5-sax-parser": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz",
+      "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parse5": "^8.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/parse5/node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+      "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "11.3.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
+      "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+      "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+      "license": "MIT"
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/piscina": {
+      "version": "5.1.4",
+      "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz",
+      "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.x"
+      },
+      "optionalDependencies": {
+        "@napi-rs/nice": "^1.0.4"
+      }
+    },
+    "node_modules/pkce-challenge": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
+      "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.20.0"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.9",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+      "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-media-query-parser": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
+      "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/postcss-safe-parser": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+      "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.31"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.8.2",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
+      "integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/proc-log": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
+      "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/promise-retry": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+      "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "err-code": "^2.0.2",
+        "retry": "^0.12.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.14.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+      "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+      "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.7.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/reflect-metadata": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+      "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/restore-cursor": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+      "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "onetime": "^7.0.0",
+        "signal-exit": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/rolldown": {
+      "version": "1.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz",
+      "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.113.0",
+        "@rolldown/pluginutils": "1.0.0-rc.4"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.0.0-rc.4",
+        "@rolldown/binding-darwin-arm64": "1.0.0-rc.4",
+        "@rolldown/binding-darwin-x64": "1.0.0-rc.4",
+        "@rolldown/binding-freebsd-x64": "1.0.0-rc.4",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4",
+        "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4",
+        "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4",
+        "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4",
+        "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4",
+        "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4",
+        "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4",
+        "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4",
+        "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.60.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+      "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.1",
+        "@rollup/rollup-android-arm64": "4.60.1",
+        "@rollup/rollup-darwin-arm64": "4.60.1",
+        "@rollup/rollup-darwin-x64": "4.60.1",
+        "@rollup/rollup-freebsd-arm64": "4.60.1",
+        "@rollup/rollup-freebsd-x64": "4.60.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+        "@rollup/rollup-linux-arm64-musl": "4.60.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+        "@rollup/rollup-linux-loong64-musl": "4.60.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+        "@rollup/rollup-linux-x64-gnu": "4.60.1",
+        "@rollup/rollup-linux-x64-musl": "4.60.1",
+        "@rollup/rollup-openbsd-x64": "4.60.1",
+        "@rollup/rollup-openharmony-arm64": "4.60.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+        "@rollup/rollup-win32-x64-gnu": "4.60.1",
+        "@rollup/rollup-win32-x64-msvc": "4.60.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/router/node_modules/path-to-regexp": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+      "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/rxjs": {
+      "version": "7.8.2",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+      "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+      "license": "Apache-2.0",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/sass": {
+      "version": "1.97.3",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
+      "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.0.2",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
+    "node_modules/sass/node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/sass/node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/saxes": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+      "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "xmlchars": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=v12.22.7"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/send/node_modules/debug/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shell-quote": {
+      "version": "1.8.3",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+      "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+      "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/sigstore": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz",
+      "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/bundle": "^4.0.0",
+        "@sigstore/core": "^3.1.0",
+        "@sigstore/protobuf-specs": "^0.5.0",
+        "@sigstore/sign": "^4.1.0",
+        "@sigstore/tuf": "^4.0.1",
+        "@sigstore/verify": "^3.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/slice-ansi": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
+      "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.3",
+        "is-fullwidth-code-point": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/smart-buffer": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+      "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks": {
+      "version": "2.8.7",
+      "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+      "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "^10.0.1",
+        "smart-buffer": "^4.2.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/socks-proxy-agent": {
+      "version": "8.0.5",
+      "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+      "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "socks": "^2.8.3"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.7.6",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+      "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/source-map-support/node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "dev": true,
+      "license": "CC-BY-3.0"
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+      "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.23",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
+      "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/ssri": {
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz",
+      "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/std-env": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+      "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/stdin-discarder": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz",
+      "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz",
+      "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.5.0",
+        "strip-ansi": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/symbol-tree": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+      "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tar": {
+      "version": "7.5.13",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
+      "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/fs-minipass": "^4.0.0",
+        "chownr": "^3.0.0",
+        "minipass": "^7.1.2",
+        "minizlib": "^3.1.0",
+        "yallist": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tar/node_modules/yallist": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+      "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
+      "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+      "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tldts": {
+      "version": "7.0.28",
+      "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
+      "integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tldts-core": "^7.0.28"
+      },
+      "bin": {
+        "tldts": "bin/cli.js"
+      }
+    },
+    "node_modules/tldts-core": {
+      "version": "7.0.28",
+      "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz",
+      "integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tough-cookie": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+      "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "tldts": "^7.0.5"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tr46": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+      "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/tree-kill": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+      "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "tree-kill": "cli.js"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD",
+      "peer": true
+    },
+    "node_modules/tuf-js": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz",
+      "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tufjs/models": "4.1.0",
+        "debug": "^4.4.3",
+        "make-fetch-happen": "^15.0.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+      "license": "MIT"
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "peer": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici": {
+      "version": "7.24.4",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
+      "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/validate-npm-package-name": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz",
+      "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.3.2",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
+      "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "esbuild": "^0.27.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest": {
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz",
+      "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vitest/expect": "4.1.4",
+        "@vitest/mocker": "4.1.4",
+        "@vitest/pretty-format": "4.1.4",
+        "@vitest/runner": "4.1.4",
+        "@vitest/snapshot": "4.1.4",
+        "@vitest/spy": "4.1.4",
+        "@vitest/utils": "4.1.4",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "4.1.4",
+        "@vitest/browser-preview": "4.1.4",
+        "@vitest/browser-webdriverio": "4.1.4",
+        "@vitest/coverage-istanbul": "4.1.4",
+        "@vitest/coverage-v8": "4.1.4",
+        "@vitest/ui": "4.1.4",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
+    "node_modules/w3c-xmlserializer": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+      "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "xml-name-validator": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/watchpack": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
+      "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "glob-to-regexp": "^0.4.1",
+        "graceful-fs": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/weak-lru-cache": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz",
+      "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/webidl-conversions": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+      "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/whatwg-mimetype": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+      "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/whatwg-url": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+      "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@exodus/bytes": "^1.11.0",
+        "tr46": "^6.0.0",
+        "webidl-conversions": "^8.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/xml-name-validator": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+      "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/xmlchars": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+      "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yargs": {
+      "version": "18.0.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
+      "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^9.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "string-width": "^7.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^22.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=23"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "22.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+      "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=23"
+      }
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yoctocolors": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
+      "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yoctocolors-cjs": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+      "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/zod": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+      "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "funding": {
+        "url": "https://github.com/sponsors/colinhacks"
+      }
+    },
+    "node_modules/zod-to-json-schema": {
+      "version": "3.25.2",
+      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+      "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+      "dev": true,
+      "license": "ISC",
+      "peerDependencies": {
+        "zod": "^3.25.28 || ^4"
+      }
+    }
+  }
+}

+ 40 - 0
package.json

@@ -0,0 +1,40 @@
+{
+  "name": "tik-tok",
+  "version": "0.0.0",
+  "scripts": {
+    "ng": "ng",
+    "start": "ng serve --proxy-config proxy.conf.json",
+    "start:dev": "ng serve --proxy-config proxy.conf.json --host 0.0.0.0",
+    "server": "node server.js",
+    "dev": "concurrently \"npm run server\" \"npm start\"",
+    "build": "ng build",
+    "watch": "ng build --watch --configuration development",
+    "test": "ng test"
+  },
+  "private": true,
+  "packageManager": "npm@11.6.1",
+  "dependencies": {
+    "@angular/common": "^21.2.0",
+    "@angular/compiler": "^21.2.0",
+    "@angular/core": "^21.2.0",
+    "@angular/forms": "^21.2.0",
+    "@angular/platform-browser": "^21.2.0",
+    "@angular/router": "^21.2.0",
+    "axios": "^1.15.0",
+    "cors": "^2.8.5",
+    "express": "^4.21.0",
+    "multer": "^2.1.1",
+    "rxjs": "~7.8.0",
+    "tslib": "^2.3.0"
+  },
+  "devDependencies": {
+    "@angular/build": "^21.2.3",
+    "@angular/cli": "^21.2.3",
+    "@angular/compiler-cli": "^21.2.0",
+    "concurrently": "^9.1.0",
+    "jsdom": "^28.0.0",
+    "prettier": "^3.8.1",
+    "typescript": "~5.9.2",
+    "vitest": "^4.0.8"
+  }
+}

+ 1 - 0
payload-stream.json

@@ -0,0 +1 @@
+{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}],"stream":true,"max_tokens":16}

+ 1 - 0
payload.json

@@ -0,0 +1 @@
+{"model":"gpt-4o-mini","messages":[{"role":"user","content":"ping"}],"stream":false,"max_tokens":16}

+ 48 - 0
proxy.conf.json

@@ -0,0 +1,48 @@
+{
+  "/api": {
+    "target": "https://api.tikhub.io",
+    "secure": true,
+    "changeOrigin": true,
+    "pathRewrite": {
+      "^/api": ""
+    },
+    "headers": {
+      "Origin": "http://localhost:4200"
+    }
+  },
+  "/jimeng": {
+    "target": "https://server.fmode.cn",
+    "secure": true,
+    "changeOrigin": true,
+    "pathRewrite": {
+      "^/jimeng": "/api/volcengine/jimeng"
+    }
+  },
+  "/parse": {
+    "target": "https://server.fmode.cn",
+    "secure": true,
+    "changeOrigin": true,
+    "pathRewrite": {
+      "^/parse": "/parse"
+    }
+  },
+  "/functions": {
+    "target": "https://server.fmode.cn",
+    "secure": true,
+    "changeOrigin": true,
+    "pathRewrite": {
+      "^/functions": "/api/functions"
+    }
+  },
+  "/backend": {
+    "target": "http://localhost:3000",
+    "secure": false,
+    "changeOrigin": true,
+    "pathRewrite": {
+      "^/backend/api": "/api",
+      "^/backend": ""
+    },
+    "timeout": 600000,
+    "proxyTimeout": 600000
+  }
+}

+ 2394 - 0
server.js

@@ -0,0 +1,2394 @@
+const express = require('express');
+const { exec, spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const cors = require('cors');
+const crypto = require('crypto');
+const multer = require('multer');
+const { Readable, Transform } = require('stream');
+const { pipeline } = require('stream/promises');
+
+const LOCAL_ENV_PATH = path.join(__dirname, '.env');
+if (fs.existsSync(LOCAL_ENV_PATH)) {
+  const envText = fs.readFileSync(LOCAL_ENV_PATH, 'utf-8');
+  for (const rawLine of envText.split(/\r?\n/)) {
+    const line = rawLine.trim();
+    if (!line || line.startsWith('#')) {
+      continue;
+    }
+
+    const separatorIndex = line.indexOf('=');
+    if (separatorIndex <= 0) {
+      continue;
+    }
+
+    const key = line.slice(0, separatorIndex).trim();
+    let value = line.slice(separatorIndex + 1).trim();
+    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
+      value = value.slice(1, -1);
+    }
+
+    if (key && process.env[key] === undefined) {
+      process.env[key] = value;
+    }
+  }
+}
+
+const app = express();
+const PORT = 3000;
+const PROJECT_ROOT = __dirname;
+const DATA_DIR = path.join(PROJECT_ROOT, 'data');
+const DATA_VIDEO_DIR = path.join(DATA_DIR, 'videos');
+const DATA_REMIX_ASSET_DIR = path.join(DATA_DIR, 'remix-assets');
+const LEGACY_VIDEO_DIR = path.join(PROJECT_ROOT, 'src', 'video');
+const VOICE_SPEAKER_ID_DOC_PATH = path.join(PROJECT_ROOT, 'docs', '音色创建', 'speaker_id.md');
+const MANIFEST_PATH = path.join(DATA_DIR, 'manifest.json');
+const LEGACY_MANIFEST_PATH = path.join(LEGACY_VIDEO_DIR, 'manifest.json');
+const WHISPER_DIR = path.join(PROJECT_ROOT, 'Whisper');
+const downloadTasks = new Map();
+const QINIU_ACCESS_KEY = process.env.QINIU_AK || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE';
+const QINIU_SECRET_KEY = process.env.QINIU_SK || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX';
+const QINIU_BUCKET = 'nova-repos';
+const QINIU_CDN_DOMAIN = 'https://repos.fmode.cn';
+const QINIU_CDN_PREFIX = 'x/openclaw-skills';
+const QINIU_UPLOAD_URL = 'https://up-z2.qiniup.com';
+const VOLC_SPEECH_BASE_URL = 'https://openspeech.bytedance.com';
+const VOLC_TTS_PROXY_BASE_URL = process.env.VOLC_TTS_PROXY_BASE_URL || 'https://server.fmode.cn/api/volcengine/tts';
+const VOLC_SPEECH_API_KEY = process.env.VOLC_SPEECH_API_KEY || process.env.BYTEDANCE_SPEECH_API_KEY || process.env.VOICE_CREATION_API_KEY || 'f112a70a-4754-4d73-8a14-a05f9b57bf65';
+const VOLC_TTS_RESOURCE_ID = process.env.VOLC_TTS_RESOURCE_ID || '';
+const VOLC_SPEECH_APP_KEY = process.env.VOLC_SPEECH_APP_KEY || process.env.VOLC_SPEECH_APP_ID || '';
+const VOLC_SPEECH_ACCESS_KEY = process.env.VOLC_SPEECH_ACCESS_KEY || '';
+
+app.use(cors());
+app.use(express.json({ limit: '50mb' }));
+
+// 静态文件:提供视频文件的访问
+const staticVideoOptions = {
+  setHeaders: (res, filePath) => {
+    if (filePath.match(/\.(mp4|mov|webm|mkv|avi)$/i)) {
+      res.setHeader('Content-Type', 'video/mp4');
+      res.setHeader('Accept-Ranges', 'bytes');
+    }
+  }
+};
+app.use('/api/video', express.static(DATA_VIDEO_DIR, staticVideoOptions));
+app.use('/api/video', express.static(LEGACY_VIDEO_DIR, staticVideoOptions));
+
+// ==================== 文件上传配置 ====================
+
+const uploadStorage = multer.diskStorage({
+  destination: (req, file, cb) => {
+    const uploadDir = DATA_VIDEO_DIR;
+    if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
+    cb(null, uploadDir);
+  },
+  filename: (req, file, cb) => {
+    // 保留原始文件名,如有冲突则加时间戳
+    const originalName = Buffer.from(file.originalname, 'latin1').toString('utf8');
+    const ext = path.extname(originalName);
+    const baseName = path.basename(originalName, ext);
+    const targetPath = path.join(DATA_VIDEO_DIR, originalName);
+    if (fs.existsSync(targetPath)) {
+      cb(null, `${baseName}-${Date.now()}${ext}`);
+    } else {
+      cb(null, originalName);
+    }
+  }
+});
+
+const assetUpload = multer({
+  storage: multer.memoryStorage(),
+  limits: { fileSize: 50 * 1024 * 1024 }
+});
+
+const upload = multer({
+  storage: uploadStorage,
+  limits: { fileSize: 500 * 1024 * 1024 }, // 500MB
+  fileFilter: (req, file, cb) => {
+    const allowedTypes = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm', 'video/x-matroska'];
+    if (allowedTypes.includes(file.mimetype) || file.originalname.match(/\.(mp4|mov|avi|webm|mkv)$/i)) {
+      cb(null, true);
+    } else {
+      cb(new Error('仅支持视频文件(mp4, mov, avi, webm, mkv)'));
+    }
+  }
+});
+
+// ==================== 工具函数 ====================
+
+// 确保 data 目录存在
+if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
+if (!fs.existsSync(DATA_VIDEO_DIR)) fs.mkdirSync(DATA_VIDEO_DIR, { recursive: true });
+if (!fs.existsSync(DATA_REMIX_ASSET_DIR)) fs.mkdirSync(DATA_REMIX_ASSET_DIR, { recursive: true });
+
+function ensureManifestFile() {
+  if (fs.existsSync(MANIFEST_PATH)) return;
+  if (fs.existsSync(LEGACY_MANIFEST_PATH)) {
+    fs.copyFileSync(LEGACY_MANIFEST_PATH, MANIFEST_PATH);
+    return;
+  }
+  fs.writeFileSync(MANIFEST_PATH, '[]', 'utf-8');
+}
+
+function readManifest() {
+  ensureManifestFile();
+  const raw = fs.readFileSync(MANIFEST_PATH, 'utf-8');
+  return JSON.parse(raw);
+}
+
+function writeManifest(data) {
+  ensureManifestFile();
+  fs.writeFileSync(MANIFEST_PATH, JSON.stringify(data, null, 2), 'utf-8');
+}
+
+function resolveVideoPath(filename) {
+  const runtimePath = path.join(DATA_VIDEO_DIR, filename);
+  if (fs.existsSync(runtimePath)) return runtimePath;
+
+  const legacyPath = path.join(LEGACY_VIDEO_DIR, filename);
+  if (fs.existsSync(legacyPath)) return legacyPath;
+
+  return runtimePath;
+}
+
+function removeFileIfExists(filePath) {
+  try {
+    if (filePath && fs.existsSync(filePath)) {
+      fs.unlinkSync(filePath);
+    }
+  } catch (err) {
+    // 忽略竞态条件 / 权限问题导致的删除失败,避免 ENOENT 等异常中断主流程
+    if (err && err.code !== 'ENOENT') {
+      console.warn(`⚠️ 删除文件失败 ${filePath}: ${err.message}`);
+    }
+  }
+}
+
+function removeDirectoryIfExists(dirPath) {
+  if (fs.existsSync(dirPath)) {
+    fs.rmSync(dirPath, { recursive: true, force: true });
+  }
+}
+
+function toBase64Url(input) {
+  return Buffer.from(input)
+    .toString('base64')
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_');
+}
+
+function buildQiniuUploadToken(key) {
+  const deadline = Math.floor(Date.now() / 1000) + 3600;
+  const putPolicy = {
+    scope: `${QINIU_BUCKET}:${key}`,
+    deadline
+  };
+  const encodedPutPolicy = toBase64Url(JSON.stringify(putPolicy));
+  const sign = crypto
+    .createHmac('sha1', QINIU_SECRET_KEY)
+    .update(encodedPutPolicy)
+    .digest('base64')
+    .replace(/\+/g, '-')
+    .replace(/\//g, '_');
+
+  return `${QINIU_ACCESS_KEY}:${sign}:${encodedPutPolicy}`;
+}
+
+function buildDigitalHumanAssetKey(fileName, kind) {
+  const rawName = Buffer.from(fileName || `asset-${Date.now()}`, 'latin1').toString('utf8');
+  const ext = path.extname(rawName).toLowerCase();
+  const safeExt = ext && /^[.a-z0-9]+$/i.test(ext) ? ext : '';
+  const baseName = path.basename(rawName, ext).replace(/[^a-zA-Z0-9_-]/g, '_') || `asset-${Date.now()}`;
+  const date = new Date();
+  const yyyy = date.getFullYear();
+  const mm = String(date.getMonth() + 1).padStart(2, '0');
+  const dd = String(date.getDate()).padStart(2, '0');
+  const timestamp = `${yyyy}${mm}${dd}-${Date.now()}`;
+  return `${QINIU_CDN_PREFIX}/digital-human/${kind}/${yyyy}${mm}${dd}/${timestamp}-${baseName}${safeExt}`;
+}
+
+function createRequestId() {
+  return typeof crypto.randomUUID === 'function'
+    ? crypto.randomUUID()
+    : `${Date.now()}-${crypto.randomBytes(8).toString('hex')}`;
+}
+
+function buildSpeechHeaders(contentType = 'application/json') {
+  const headers = {
+    'Content-Type': contentType,
+    'X-Api-Request-Id': createRequestId()
+  };
+
+  if (VOLC_SPEECH_API_KEY) {
+    headers['X-Api-Key'] = VOLC_SPEECH_API_KEY;
+    return headers;
+  }
+
+  if (VOLC_SPEECH_APP_KEY && VOLC_SPEECH_ACCESS_KEY) {
+    headers['X-Api-App-Key'] = VOLC_SPEECH_APP_KEY;
+    headers['X-Api-Access-Key'] = VOLC_SPEECH_ACCESS_KEY;
+    return headers;
+  }
+
+  throw new Error('未配置火山语音鉴权,请设置 VOLC_SPEECH_API_KEY 或 VOLC_SPEECH_APP_KEY + VOLC_SPEECH_ACCESS_KEY');
+}
+
+function normalizeOfficialSpeechResourceId(value) {
+  const resourceId = String(value || '').trim();
+  return /^seed-(tts|icl)-/i.test(resourceId) ? resourceId : '';
+}
+
+function inferOfficialSpeechResourceId(speaker, requestedResourceId = '') {
+  const explicitResourceId = normalizeOfficialSpeechResourceId(requestedResourceId);
+  if (explicitResourceId) {
+    return explicitResourceId;
+  }
+
+  const configuredResourceId = normalizeOfficialSpeechResourceId(VOLC_TTS_RESOURCE_ID);
+  if (configuredResourceId) {
+    return configuredResourceId;
+  }
+
+  const normalizedSpeaker = String(speaker || '').trim();
+  if (/^(S_|icl_|saturn_|dit_)/i.test(normalizedSpeaker)) {
+    return 'seed-icl-2.0';
+  }
+
+  return 'seed-tts-2.0';
+}
+
+function clampNumber(value, fallback, min, max) {
+  const numericValue = Number(value);
+  if (!Number.isFinite(numericValue)) {
+    return fallback;
+  }
+
+  return Math.min(max, Math.max(min, numericValue));
+}
+
+function clampOptionalNumber(value, min, max) {
+  if (value === null || value === undefined || String(value).trim() === '') {
+    return null;
+  }
+
+  const numericValue = Number(value);
+  if (!Number.isFinite(numericValue)) {
+    return null;
+  }
+
+  return Math.min(max, Math.max(min, numericValue));
+}
+
+function parseOptionalBoolean(value, fallback = false) {
+  if (value === null || value === undefined || value === '') {
+    return fallback;
+  }
+
+  if (typeof value === 'boolean') {
+    return value;
+  }
+
+  if (typeof value === 'number') {
+    return value !== 0;
+  }
+
+  const normalizedValue = String(value).trim().toLowerCase();
+  if (['true', '1', 'yes', 'on'].includes(normalizedValue)) {
+    return true;
+  }
+
+  if (['false', '0', 'no', 'off'].includes(normalizedValue)) {
+    return false;
+  }
+
+  return fallback;
+}
+
+function normalizeVoiceSynthesisErrorMessage(message) {
+  const rawMessage = String(message || '').trim();
+  if (/resource ID is mismatched with speaker related resource/i.test(rawMessage)) {
+    return '当前本地 X-Api-Key 与所选 speaker_id 不属于同一语音资源,无法直接使用 speaker_id 合成。请先通过音色复刻获取 timbreId 后再合成,或更换与该 speaker_id 匹配的 X-Api-Key。';
+  }
+
+  return rawMessage || '语音合成失败';
+}
+
+function mapVoiceDesignStatus(status) {
+  switch (Number(status)) {
+    case 0:
+      return '未找到';
+    case 1:
+      return '训练中';
+    case 2:
+      return '可用';
+    case 3:
+      return '失败';
+    case 4:
+      return '已激活';
+    default:
+      return '未知';
+  }
+}
+
+function mapVoiceCloneStatus(status) {
+  switch (String(status ?? '')) {
+    case '0':
+      return '未占用';
+    case '1':
+      return '可用';
+    case '2':
+      return '训练中';
+    case '3':
+      return '失败';
+    case '404':
+      return '已删除';
+    default:
+      return '未知';
+  }
+}
+
+function normalizeBearerToken(token) {
+  const rawToken = String(token || '').trim();
+  if (!rawToken) {
+    return '';
+  }
+
+  return /^Bearer\s+/i.test(rawToken) ? rawToken : `Bearer ${rawToken}`;
+}
+
+function inferAudioFormat(file) {
+  const mimeType = String(file?.mimetype || '').toLowerCase();
+  if (mimeType.includes('mpeg') || mimeType.includes('mp3')) {
+    return 'mp3';
+  }
+  if (mimeType.includes('wav')) {
+    return 'wav';
+  }
+  if (mimeType.includes('m4a') || mimeType.includes('mp4')) {
+    return 'm4a';
+  }
+  if (mimeType.includes('aac')) {
+    return 'aac';
+  }
+  if (mimeType.includes('flac')) {
+    return 'flac';
+  }
+  if (mimeType.includes('ogg') || mimeType.includes('opus')) {
+    return 'ogg_opus';
+  }
+  if (mimeType.includes('pcm')) {
+    return 'pcm';
+  }
+
+  const ext = path.extname(file?.originalname || '').toLowerCase();
+  switch (ext) {
+    case '.wav':
+      return 'wav';
+    case '.m4a':
+      return 'm4a';
+    case '.aac':
+      return 'aac';
+    case '.flac':
+      return 'flac';
+    case '.ogg':
+    case '.opus':
+      return 'ogg_opus';
+    case '.pcm':
+      return 'pcm';
+    case '.mp3':
+    default:
+      return 'mp3';
+  }
+}
+
+async function readFetchResponse(response) {
+  const rawText = await response.text();
+  let payload = null;
+  try {
+    payload = rawText ? JSON.parse(rawText) : null;
+  } catch {
+    payload = null;
+  }
+
+  return {
+    rawText,
+    payload
+  };
+}
+
+function extractPrimaryVoiceModel(timbre) {
+  if (!Array.isArray(timbre?.models) || timbre.models.length === 0) {
+    return {};
+  }
+
+  return timbre.models[0] || {};
+}
+
+function readVoiceSpeakerIdOptions() {
+  if (!fs.existsSync(VOICE_SPEAKER_ID_DOC_PATH)) {
+    return [];
+  }
+
+  const rawText = fs.readFileSync(VOICE_SPEAKER_ID_DOC_PATH, 'utf-8');
+  const seen = new Set();
+  const options = [];
+
+  for (const rawLine of rawText.split(/\r?\n/)) {
+    const speakerId = rawLine.trim().match(/^S_[A-Za-z0-9]+$/)?.[0] || '';
+    if (!speakerId || seen.has(speakerId)) {
+      continue;
+    }
+    seen.add(speakerId);
+    options.push(speakerId);
+  }
+
+  return options;
+}
+
+function upsertVoiceProfile(profile) {
+  const profiles = readDataFile('voice-profiles');
+  const existingIndex = profiles.findIndex((item) => (
+    (!!profile.timbre_id && item.timbre_id === profile.timbre_id)
+    || (!!profile.id && item.id === profile.id)
+  ));
+
+  if (existingIndex >= 0) {
+    const existingProfile = profiles[existingIndex];
+    const nextProfile = {
+      ...existingProfile,
+      ...profile,
+      id: existingProfile.id || profile.id,
+      created_at: existingProfile.created_at || profile.created_at,
+      updated_at: profile.updated_at || new Date().toISOString()
+    };
+    profiles[existingIndex] = nextProfile;
+    writeDataFile('voice-profiles', profiles);
+    return nextProfile;
+  }
+
+  profiles.unshift(profile);
+  writeDataFile('voice-profiles', profiles);
+  return profile;
+}
+
+async function requestVolcTtsJson(endpoint, requestBody) {
+  const response = await fetch(`${VOLC_TTS_PROXY_BASE_URL}/${endpoint}`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(requestBody)
+  });
+
+  const { rawText, payload } = await readFetchResponse(response);
+  if (!response.ok) {
+    const error = new Error(payload?.error?.message || payload?.message || payload?.error || rawText || 'TTS 请求失败');
+    error.status = response.status;
+    error.detail = payload || rawText || '';
+    throw error;
+  }
+
+  return payload;
+}
+
+async function proxyVolcTtsStream(endpoint, requestBody, res) {
+  const response = await fetch(`${VOLC_TTS_PROXY_BASE_URL}/${endpoint}`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json'
+    },
+    body: JSON.stringify(requestBody)
+  });
+
+  if (!response.ok) {
+    const { rawText, payload } = await readFetchResponse(response);
+    const error = new Error(payload?.error?.message || payload?.message || payload?.error || rawText || 'TTS 流式请求失败');
+    error.status = response.status;
+    error.detail = payload || rawText || '';
+    throw error;
+  }
+
+  res.status(response.status);
+  res.setHeader('Content-Type', response.headers.get('content-type') || 'application/x-ndjson; charset=utf-8');
+  res.setHeader('Cache-Control', 'no-cache, no-transform');
+
+  if (!response.body) {
+    res.end();
+    return;
+  }
+
+  await pipeline(Readable.fromWeb(response.body), res);
+}
+
+async function synthesizeVoiceAudio({ token, speaker, timbreId, text, ssml, xApiResourceId, model, isStream = false, audioParams = {}, additions = {} }) {
+  const normalizedSpeaker = String(speaker || '').trim();
+  const fallbackSpeaker = String(timbreId || '').trim();
+  const resolvedSpeaker = normalizedSpeaker || fallbackSpeaker;
+  const requestedResourceId = String(xApiResourceId || '').trim();
+  const headerResourceId = inferOfficialSpeechResourceId(resolvedSpeaker, requestedResourceId);
+  const resolvedModel = String(model || '').trim();
+  const serializedAdditions = Object.keys(additions).length > 0 ? JSON.stringify(additions) : '';
+  const requestBody = {
+    req_params: {
+      ...(ssml ? { ssml } : { text }),
+      speaker: resolvedSpeaker,
+      audio_params: {
+        format: audioParams.format || 'mp3',
+        sample_rate: audioParams.sampleRate ?? 24000,
+        ...(audioParams.speechRate === null || audioParams.speechRate === undefined ? {} : { speech_rate: audioParams.speechRate }),
+        ...(audioParams.loudnessRate === null || audioParams.loudnessRate === undefined ? {} : { loudness_rate: audioParams.loudnessRate }),
+        ...(audioParams.emotion ? { emotion: audioParams.emotion } : {}),
+        ...(audioParams.emotionScale === null || audioParams.emotionScale === undefined ? {} : { emotion_scale: audioParams.emotionScale }),
+        ...(audioParams.enableSubtitle ? { enable_subtitle: true } : {})
+      },
+      ...(serializedAdditions ? { additions: serializedAdditions } : {}),
+      ...(resolvedModel ? { model: resolvedModel } : {})
+    }
+  };
+
+  const headers = buildSpeechHeaders();
+  headers['X-Api-Resource-Id'] = headerResourceId;
+  headers['Connection'] = 'keep-alive';
+
+  const response = await fetch(`${VOLC_SPEECH_BASE_URL}/api/v3/tts/unidirectional`, {
+    method: 'POST',
+    headers,
+    body: JSON.stringify(requestBody)
+  });
+
+  const { rawText, payload } = await readFetchResponse(response);
+  if (!response.ok) {
+    const error = new Error(normalizeVoiceSynthesisErrorMessage(payload?.error?.message || payload?.message || payload?.error || rawText || 'TTS 请求失败'));
+    error.status = response.status;
+    error.detail = payload || rawText || '';
+    throw error;
+  }
+
+  if (Number(payload?.code) !== 200 || !payload?.data?.audioUrl) {
+    const error = new Error(normalizeVoiceSynthesisErrorMessage(payload?.error?.message || payload?.message || '语音合成失败'));
+    error.status = 502;
+    error.detail = payload || '';
+    throw error;
+  }
+
+  return {
+    audioUrl: payload.data.audioUrl,
+    workId: payload.data.workId || '',
+    payload
+  };
+}
+
+function createHttpError(message, status = 500, detail = '') {
+  const error = new Error(message || '请求失败');
+  error.status = status;
+  error.detail = detail;
+  return error;
+}
+
+function inferAudioFormatFromUrl(url) {
+  try {
+    const parsed = new URL(String(url || '').trim());
+    return inferAudioFormat({ originalname: parsed.pathname || '', mimetype: '' });
+  } catch {
+    return inferAudioFormat({ originalname: String(url || '').trim(), mimetype: '' });
+  }
+}
+
+function normalizeVoiceClonePayload(body) {
+  const audioData = body?.audioData && typeof body.audioData === 'object' ? body.audioData : {};
+  const extraParams = body?.extra_params && typeof body.extra_params === 'object' ? body.extra_params : {};
+  return {
+    token: normalizeBearerToken(body?.token),
+    name: String(body?.name || body?.displayName || '').trim(),
+    timbreId: String(body?.timbreId || '').trim(),
+    speakerId: String(body?.speaker_id || body?.speakerId || '').trim(),
+    audioData: {
+      url: String(audioData?.url || '').trim(),
+      base64: String(audioData?.base64 || '').trim(),
+      format: String(audioData?.format || '').trim().toLowerCase(),
+      text: String(audioData?.text || body?.audioText || '').trim()
+    },
+    language: Number(body?.language ?? 0) === 1 ? 1 : 0,
+    demoText: String(extraParams?.demo_text || body?.sampleText || '').trim(),
+    sourceAudioName: String(body?.sourceAudioName || body?.source_audio_name || '').trim(),
+    sourceAudioFormat: String(body?.sourceAudioFormat || body?.source_audio_format || '').trim().toLowerCase()
+  };
+}
+
+function normalizeVoiceSynthesisPayload(body) {
+  const rawAudioParams = body?.audio_params && typeof body.audio_params === 'object'
+    ? body.audio_params
+    : (body?.audioParams && typeof body.audioParams === 'object' ? body.audioParams : {});
+  const rawAdditions = body?.additions && typeof body.additions === 'object' ? body.additions : {};
+  const formatCandidate = String(rawAudioParams?.format || '').trim().toLowerCase();
+  const format = ['mp3', 'ogg_opus', 'pcm'].includes(formatCandidate) ? formatCandidate : 'mp3';
+  const sampleRateCandidate = Number(rawAudioParams?.sampleRate ?? rawAudioParams?.sample_rate);
+  const sampleRate = [8000, 16000, 22050, 24000, 32000, 44100, 48000].includes(sampleRateCandidate) ? sampleRateCandidate : 24000;
+  const speechRate = clampNumber(rawAudioParams?.speechRate ?? rawAudioParams?.speech_rate, 0, -50, 100);
+  const loudnessRate = clampNumber(rawAudioParams?.loudnessRate ?? rawAudioParams?.loudness_rate, 0, -50, 100);
+  const emotion = String(rawAudioParams?.emotion || '').trim();
+  const emotionScale = clampOptionalNumber(rawAudioParams?.emotionScale ?? rawAudioParams?.emotion_scale, 1, 5);
+  const enableSubtitle = parseOptionalBoolean(rawAudioParams?.enableSubtitle ?? rawAudioParams?.enable_subtitle, false);
+  const silenceDuration = clampOptionalNumber(rawAdditions?.silenceDuration ?? rawAdditions?.silence_duration, 0, 30000);
+  const enableLanguageDetector = parseOptionalBoolean(rawAdditions?.enableLanguageDetector ?? rawAdditions?.enable_language_detector, false);
+  const disableMarkdownFilter = parseOptionalBoolean(rawAdditions?.disableMarkdownFilter ?? rawAdditions?.disable_markdown_filter, false);
+  const disableEmojiFilter = parseOptionalBoolean(rawAdditions?.disableEmojiFilter ?? rawAdditions?.disable_emoji_filter, false);
+  const explicitLanguage = String((rawAdditions?.explicitLanguage ?? rawAdditions?.explicit_language) || '').trim();
+  return {
+    token: normalizeBearerToken(body?.token),
+    text: String(body?.text || '').trim(),
+    ssml: String(body?.ssml || '').trim(),
+    timbreId: String(body?.timbreId || body?.timbre_id || '').trim(),
+    speakerId: String(body?.speaker_id || body?.speakerId || body?.speaker || '').trim(),
+    isStream: parseOptionalBoolean(body?.isStream ?? body?.is_stream, true),
+    xApiResourceId: String(body?.x_api_resource_id || body?.xApiResourceId || '').trim(),
+    model: String(body?.model || '').trim(),
+    audioParams: {
+      format,
+      sampleRate,
+      speechRate,
+      loudnessRate,
+      emotion,
+      emotionScale,
+      enableSubtitle
+    },
+    additions: {
+      ...(silenceDuration === null ? {} : { silence_duration: silenceDuration }),
+      ...(enableLanguageDetector ? { enable_language_detector: true } : {}),
+      ...(disableMarkdownFilter ? { disable_markdown_filter: true } : {}),
+      ...(disableEmojiFilter ? { disable_emoji_filter: true } : {}),
+      ...(explicitLanguage ? { explicit_language: explicitLanguage } : {})
+    }
+  };
+}
+
+function buildProxySynthesisPayload(payload) {
+  return {
+    token: payload.token,
+    ...(payload.text ? { text: payload.text } : {}),
+    ...(payload.ssml ? { ssml: payload.ssml } : {}),
+    ...(payload.timbreId ? { timbreId: payload.timbreId } : {}),
+    ...(payload.speakerId ? { speaker_id: payload.speakerId } : {}),
+    isStream: !!payload.isStream,
+    ...(payload.xApiResourceId ? { x_api_resource_id: payload.xApiResourceId } : {}),
+    ...(payload.model ? { model: payload.model } : {}),
+    audio_params: {
+      format: payload.audioParams.format,
+      sample_rate: payload.audioParams.sampleRate,
+      speech_rate: payload.audioParams.speechRate,
+      loudness_rate: payload.audioParams.loudnessRate,
+      ...(payload.audioParams.emotion ? { emotion: payload.audioParams.emotion } : {}),
+      ...(payload.audioParams.emotionScale === null || payload.audioParams.emotionScale === undefined ? {} : { emotion_scale: payload.audioParams.emotionScale }),
+      ...(payload.audioParams.enableSubtitle ? { enable_subtitle: true } : {})
+    },
+    ...(Object.keys(payload.additions).length > 0 ? { additions: payload.additions } : {})
+  };
+}
+
+async function executeVoiceCloneRequest(payload) {
+  if (!payload.token) {
+    throw createHttpError('缺少 token 参数', 400);
+  }
+  if (!payload.name) {
+    throw createHttpError('缺少音色名称 name', 400);
+  }
+  if (!payload.timbreId && !payload.speakerId) {
+    throw createHttpError('timbreId与speaker_id不能同时为空', 400);
+  }
+  if (!payload.audioData.url && !payload.audioData.base64) {
+    throw createHttpError('音频url或base64至少提供一个', 400);
+  }
+  if (payload.demoText && (payload.demoText.length < 4 || payload.demoText.length > 80)) {
+    throw createHttpError('试听文本长度需在 4-80 字之间', 400);
+  }
+
+  const clonePayload = await requestVolcTtsJson('voice_clone', {
+    token: payload.token,
+    name: payload.name,
+    ...(payload.timbreId ? { timbreId: payload.timbreId } : {}),
+    ...(payload.speakerId ? { speaker_id: payload.speakerId } : {}),
+    audioData: {
+      ...(payload.audioData.url ? { url: payload.audioData.url } : {}),
+      ...(payload.audioData.base64 ? { base64: payload.audioData.base64 } : {}),
+      ...(payload.audioData.format ? { format: payload.audioData.format } : {}),
+      ...(payload.audioData.text ? { text: payload.audioData.text } : {})
+    },
+    language: payload.language,
+    ...(payload.demoText ? { extra_params: { demo_text: payload.demoText } } : {})
+  });
+
+  if (Number(clonePayload?.code) !== 200 || !clonePayload?.data?.timbre?.objectId) {
+    throw createHttpError(clonePayload?.error?.message || clonePayload?.message || '音色复刻失败', 502, clonePayload || '');
+  }
+
+  const timbre = clonePayload.data.timbre;
+  const primaryModel = extractPrimaryVoiceModel(timbre);
+  const now = new Date().toISOString();
+  const profile = upsertVoiceProfile({
+    id: `VOICE-${Date.now()}`,
+    name: payload.name,
+    creation_mode: 'clone',
+    speaker_id: timbre?.speaker_id || payload.speakerId,
+    timbre_id: timbre?.objectId || payload.timbreId || '',
+    sample_text: payload.demoText,
+    source_audio_text: payload.audioData.text,
+    source_audio_name: payload.sourceAudioName,
+    source_audio_format: payload.sourceAudioFormat || payload.audioData.format || inferAudioFormatFromUrl(payload.audioData.url),
+    text_prompt: '',
+    language: payload.language,
+    status: Number.isFinite(Number(timbre?.status)) ? Number(timbre.status) : null,
+    status_label: mapVoiceCloneStatus(timbre?.status),
+    demo_audio: primaryModel?.demo_audio || '',
+    available_training_times: null,
+    image_prompt_name: '',
+    x_api_resource_id: Array.isArray(primaryModel?.x_api_resource_id) ? String(primaryModel.x_api_resource_id[0] || '') : '',
+    model_version: String(primaryModel?.version || ''),
+    icl_speaker_id: String(primaryModel?.icl_speaker_id || ''),
+    occupied: !!timbre?.occupied,
+    synthesized_audio_url: '',
+    synthesized_work_id: '',
+    last_synthesis_text: '',
+    latest_audio_url: primaryModel?.demo_audio || '',
+    message: clonePayload?.data?.tip || '音色复刻成功',
+    request_id: clonePayload?.request_id || '',
+    created_at: now,
+    updated_at: now
+  });
+
+  return {
+    clonePayload,
+    profile
+  };
+}
+
+function findVoiceProfileForSynthesis(payload) {
+  const profiles = readDataFile('voice-profiles');
+  if (payload.timbreId) {
+    const byTimbreId = profiles.find((item) => item.timbre_id === payload.timbreId);
+    if (byTimbreId) {
+      return byTimbreId;
+    }
+  }
+
+  if (payload.speakerId) {
+    return profiles.find((item) => (
+      String(item.icl_speaker_id || '').trim() === payload.speakerId
+      || String(item.speaker_id || '').trim() === payload.speakerId
+    )) || null;
+  }
+
+  return null;
+}
+
+function persistSynthesisProfile(profile, payload, synthesis) {
+  if (!profile) {
+    return null;
+  }
+
+  return upsertVoiceProfile({
+    ...profile,
+    synthesized_audio_url: synthesis.audioUrl,
+    synthesized_work_id: synthesis.workId,
+    last_synthesis_text: payload.text,
+    last_synthesis_ssml: payload.ssml,
+    last_synthesis_x_api_resource_id: payload.xApiResourceId || profile.x_api_resource_id || '',
+    last_synthesis_model: payload.model,
+    last_synthesis_format: payload.audioParams.format,
+    last_synthesis_sample_rate: payload.audioParams.sampleRate,
+    last_synthesis_speech_rate: payload.audioParams.speechRate,
+    last_synthesis_loudness_rate: payload.audioParams.loudnessRate,
+    last_synthesis_emotion: payload.audioParams.emotion,
+    last_synthesis_emotion_scale: payload.audioParams.emotionScale,
+    last_synthesis_enable_subtitle: !!payload.audioParams.enableSubtitle,
+    last_synthesis_silence_duration: payload.additions?.silence_duration ?? null,
+    last_synthesis_enable_language_detector: !!payload.additions?.enable_language_detector,
+    last_synthesis_disable_markdown_filter: !!payload.additions?.disable_markdown_filter,
+    last_synthesis_disable_emoji_filter: !!payload.additions?.disable_emoji_filter,
+    last_synthesis_explicit_language: String(payload.additions?.explicit_language || '').trim(),
+    latest_audio_url: synthesis.audioUrl || profile.latest_audio_url || profile.demo_audio || '',
+    updated_at: new Date().toISOString()
+  });
+}
+
+async function executeVoiceSynthesisRequest(payload) {
+  if (!payload.token) {
+    throw createHttpError('缺少 token 参数', 400);
+  }
+  if (!payload.text && !payload.ssml) {
+    throw createHttpError('文本内容不能为空,text与ssml不能同时为空', 400);
+  }
+
+  const matchedProfile = findVoiceProfileForSynthesis(payload);
+
+  if (payload.timbreId) {
+    const proxyPayload = await requestVolcTtsJson('unidirectional', buildProxySynthesisPayload({
+      ...payload,
+      isStream: false
+    }));
+
+    if (Number(proxyPayload?.code) !== 200 || !proxyPayload?.data?.audioUrl) {
+      throw createHttpError(proxyPayload?.error?.message || proxyPayload?.message || '语音合成失败', 502, proxyPayload || '');
+    }
+
+    const profile = persistSynthesisProfile(matchedProfile, payload, {
+      audioUrl: proxyPayload.data.audioUrl,
+      workId: proxyPayload.data.workId || ''
+    });
+
+    return {
+      response: {
+        ...proxyPayload,
+        ...(profile ? { profile } : {})
+      },
+      profile
+    };
+  }
+
+  if (!payload.speakerId) {
+    throw createHttpError('音色id不能为空', 400);
+  }
+
+  const synthesis = await synthesizeVoiceAudio({
+    token: payload.token,
+    speaker: payload.speakerId,
+    timbreId: matchedProfile?.timbre_id || '',
+    text: payload.text,
+    ssml: payload.ssml,
+    xApiResourceId: payload.xApiResourceId || matchedProfile?.x_api_resource_id || '',
+    model: payload.model,
+    isStream: false,
+    audioParams: payload.audioParams,
+    additions: payload.additions
+  });
+
+  const profile = persistSynthesisProfile(matchedProfile, payload, synthesis);
+
+  return {
+    response: {
+      code: 200,
+      data: {
+        workId: synthesis.workId,
+        audioUrl: synthesis.audioUrl
+      },
+      ...(profile ? { profile } : {})
+    },
+    profile
+  };
+}
+
+// 通用 JSON 数据文件读写
+function readDataFile(name) {
+  const p = path.join(DATA_DIR, `${name}.json`);
+  if (!fs.existsSync(p)) { fs.writeFileSync(p, '[]', 'utf-8'); return []; }
+  return JSON.parse(fs.readFileSync(p, 'utf-8'));
+}
+
+function writeDataFile(name, data) {
+  fs.writeFileSync(path.join(DATA_DIR, `${name}.json`), JSON.stringify(data, null, 2), 'utf-8');
+}
+
+function isSafeRemoteUrl(url) {
+  try {
+    const parsed = new URL(url);
+    return ['http:', 'https:'].includes(parsed.protocol);
+  } catch {
+    return false;
+  }
+}
+
+function sanitizeFilename(filename) {
+  const safeName = String(filename || '')
+    .replace(/[<>:"/\\|?*\x00-\x1F]/g, '_')
+    .trim();
+
+  return safeName || `video-${Date.now()}.mp4`;
+}
+
+function ensureVideoFilename(filename, sourceUrl = '') {
+  const safeName = sanitizeFilename(filename);
+  if (path.extname(safeName)) {
+    return safeName;
+  }
+
+  try {
+    const parsed = new URL(sourceUrl);
+    const sourceExt = path.extname(parsed.pathname || '').toLowerCase();
+    if (sourceExt) {
+      return `${safeName}${sourceExt}`;
+    }
+  } catch {}
+
+  return `${safeName}.mp4`;
+}
+
+function ensureUniqueVideoFilename(filename) {
+  const ext = path.extname(filename) || '.mp4';
+  const baseName = path.basename(filename, ext);
+  let candidate = filename;
+  let counter = 1;
+
+  while (fs.existsSync(path.join(DATA_VIDEO_DIR, candidate)) || fs.existsSync(path.join(LEGACY_VIDEO_DIR, candidate))) {
+    candidate = `${baseName}-${Date.now()}-${counter}${ext}`;
+    counter += 1;
+  }
+
+  return candidate;
+}
+
+function createManagedVideoEntry({ filename, title, description, tags, thumbnail, duration, resolution, awemeId, authorName, size }) {
+  const ext = path.extname(filename).replace('.', '').toLowerCase() || 'mp4';
+  const now = new Date().toISOString();
+
+  return {
+    id: `VID-${Date.now()}`,
+    title: title || path.basename(filename, path.extname(filename)),
+    filename,
+    size: size || 0,
+    duration: Number(duration) || 0,
+    created_at: now,
+    modified_at: now,
+    category: 'downloaded',
+    tags: Array.isArray(tags) ? tags : [],
+    description: description || '',
+    thumbnail: thumbnail || '',
+    source: 'downloaded',
+    aweme_id: awemeId || '',
+    metadata: {
+      resolution: resolution || '未知',
+      format: ext,
+      authorName: authorName || ''
+    }
+  };
+}
+
+function normalizeRemoteUrls(primaryUrl, urls = []) {
+  return [primaryUrl, ...(Array.isArray(urls) ? urls : [])].filter((url, index, list) => (
+    typeof url === 'string'
+    && isSafeRemoteUrl(url)
+    && list.indexOf(url) === index
+  ));
+}
+
+async function fetchRemoteVideoResponse(urls, requestHeaders = {}) {
+  let lastError = null;
+
+  for (const currentUrl of urls) {
+    try {
+      const response = await fetch(currentUrl, {
+        method: 'GET',
+        headers: requestHeaders,
+        redirect: 'follow'
+      });
+
+      if (!response.ok) {
+        const detail = await response.text().catch(() => '');
+        lastError = new Error(`远程下载失败: ${response.status} ${response.statusText}${detail ? ` ${detail.slice(0, 200)}` : ''}`);
+        continue;
+      }
+
+      if (!response.body) {
+        lastError = new Error('远程响应缺少视频流');
+        continue;
+      }
+
+      return { url: currentUrl, response };
+    } catch (error) {
+      lastError = error;
+    }
+  }
+
+  throw lastError || new Error('没有可用的远程视频地址');
+}
+
+// ==================== Whisper 转录 ====================
+
+// POST /api/whisper/transcribe
+// body: { videoId, language?, model? }
+app.post('/api/whisper/transcribe', async (req, res) => {
+  const { videoId, language = 'Chinese', model = 'base' } = req.body;
+
+  if (!videoId) {
+    return res.status(400).json({ error: '缺少 videoId 参数' });
+  }
+
+  // 从 manifest 查找视频
+  const manifest = readManifest();
+  const video = manifest.find(v => v.id === videoId);
+  if (!video) {
+    return res.status(404).json({ error: `未找到视频: ${videoId}` });
+  }
+
+  const videoPath = resolveVideoPath(video.filename);
+  if (!fs.existsSync(videoPath)) {
+    return res.status(404).json({ error: `视频文件不存在: ${video.filename}` });
+  }
+
+  // 为该视频创建专属输出目录
+  const baseName = video.filename.replace(/\.[^.]+$/, '');
+  const outputDir = path.join(WHISPER_DIR, baseName);
+  if (!fs.existsSync(outputDir)) {
+    fs.mkdirSync(outputDir, { recursive: true });
+  }
+
+  console.log(`🎙️ 开始 Whisper 转录: ${video.filename}`);
+  console.log(`   模型: ${model}, 语言: ${language}`);
+  console.log(`   输出目录: ${outputDir}`);
+
+  // 执行 Whisper 命令
+  const cmd = `whisper "${videoPath}" --model ${model} --language ${language} --output_dir "${outputDir}"`;
+
+  try {
+    const result = await new Promise((resolve, reject) => {
+      const process = exec(cmd, {
+        cwd: PROJECT_ROOT,
+        timeout: 10 * 60 * 1000, // 10分钟超时
+        maxBuffer: 10 * 1024 * 1024
+      });
+
+      let stdout = '';
+      let stderr = '';
+
+      process.stdout.on('data', (data) => {
+        stdout += data;
+        console.log(`   [whisper] ${data.toString().trim()}`);
+      });
+
+      process.stderr.on('data', (data) => {
+        stderr += data;
+      });
+
+      process.on('close', (code) => {
+        if (code === 0) {
+          resolve({ stdout, stderr });
+        } else {
+          reject(new Error(`Whisper 退出码: ${code}\n${stderr}`));
+        }
+      });
+
+      process.on('error', (err) => {
+        reject(new Error(`无法启动 Whisper: ${err.message}`));
+      });
+    });
+
+    // 读取生成的文件
+    const txtFile = path.join(outputDir, `${baseName}.txt`);
+    const srtFile = path.join(outputDir, `${baseName}.srt`);
+
+    const transcript = fs.existsSync(txtFile) ? fs.readFileSync(txtFile, 'utf-8') : '';
+    const srt = fs.existsSync(srtFile) ? fs.readFileSync(srtFile, 'utf-8') : '';
+
+    if (!transcript) {
+      return res.status(500).json({ error: 'Whisper 执行完成但未生成文字稿' });
+    }
+
+    // 更新 manifest 中该视频的 whisper 字段
+    const whisperPaths = {
+      transcript: `Whisper/${baseName}/${baseName}.txt`,
+      srt: `Whisper/${baseName}/${baseName}.srt`
+    };
+
+    // 检查是否存在其他输出文件
+    const jsonFile = path.join(outputDir, `${baseName}.json`);
+    const vttFile = path.join(outputDir, `${baseName}.vtt`);
+    const tsvFile = path.join(outputDir, `${baseName}.tsv`);
+    if (fs.existsSync(jsonFile)) whisperPaths.segments = `Whisper/${baseName}/${baseName}.json`;
+
+    video.whisper = whisperPaths;
+    writeManifest(manifest);
+
+    console.log(`✅ Whisper 转录完成: ${baseName}`);
+
+    res.json({
+      success: true,
+      videoId: video.id,
+      transcript,
+      srt,
+      whisper: whisperPaths,
+      outputDir: `Whisper/${baseName}`
+    });
+
+  } catch (err) {
+    console.error(`❌ Whisper 转录失败:`, err.message);
+    res.status(500).json({
+      error: `Whisper 转录失败: ${err.message}`,
+      hint: '请确保已安装 Whisper: pip install openai-whisper'
+    });
+  }
+});
+
+// GET /api/whisper/status — 检查 Whisper 是否可用
+app.get('/api/whisper/status', (req, res) => {
+  exec('whisper --help', { timeout: 5000 }, (err) => {
+    if (err) {
+      res.json({ available: false, message: '未检测到 Whisper,请执行: pip install openai-whisper' });
+    } else {
+      res.json({ available: true, message: 'Whisper 已安装' });
+    }
+  });
+});
+
+// ==================== Manifest 管理 ====================
+
+// GET /api/manifest — 获取完整 manifest
+app.get('/api/manifest', (req, res) => {
+  try {
+    const manifest = readManifest();
+    res.json(manifest);
+  } catch (err) {
+    res.status(500).json({ error: `读取 manifest 失败: ${err.message}` });
+  }
+});
+
+// PUT /api/manifest/:videoId — 更新指定视频条目
+app.put('/api/manifest/:videoId', (req, res) => {
+  try {
+    const manifest = readManifest();
+    const idx = manifest.findIndex(v => v.id === req.params.videoId);
+    if (idx === -1) {
+      return res.status(404).json({ error: `未找到视频: ${req.params.videoId}` });
+    }
+    // 合并更新字段
+    manifest[idx] = { ...manifest[idx], ...req.body };
+    writeManifest(manifest);
+    res.json({ success: true, video: manifest[idx] });
+  } catch (err) {
+    res.status(500).json({ error: `更新 manifest 失败: ${err.message}` });
+  }
+});
+
+// POST /api/manifest — 添加新视频条目
+app.post('/api/manifest', (req, res) => {
+  try {
+    const manifest = readManifest();
+    const newEntry = req.body;
+    if (!newEntry.id || !newEntry.filename) {
+      return res.status(400).json({ error: '缺少 id 或 filename' });
+    }
+    // 检查重复
+    if (manifest.find(v => v.id === newEntry.id)) {
+      return res.status(409).json({ error: `视频 ${newEntry.id} 已存在` });
+    }
+    manifest.push(newEntry);
+    writeManifest(manifest);
+    res.json({ success: true, video: newEntry });
+  } catch (err) {
+    res.status(500).json({ error: `添加视频失败: ${err.message}` });
+  }
+});
+
+// DELETE /api/manifest/:videoId — 删除指定视频条目及关联文件
+app.delete('/api/manifest/:videoId', (req, res) => {
+  try {
+    const manifest = readManifest();
+    const idx = manifest.findIndex(v => v.id === req.params.videoId);
+    if (idx === -1) {
+      return res.status(404).json({ error: `未找到视频: ${req.params.videoId}` });
+    }
+
+    const [video] = manifest.splice(idx, 1);
+    const filename = typeof video.filename === 'string' ? video.filename : '';
+
+    // 先清理关联文件,所有步骤都做容错;最后再持久化 manifest,避免中途异常导致状态不一致
+    if (filename) {
+      try {
+        const videoPath = resolveVideoPath(filename);
+        removeFileIfExists(videoPath);
+      } catch (cleanupErr) {
+        console.warn(`⚠️ 删除视频文件失败 (${filename}):`, cleanupErr.message);
+      }
+    } else {
+      console.warn(`⚠️ 视频条目 ${video.id} 缺少 filename 字段,跳过文件清理`);
+    }
+
+    try {
+      const remixFilePath = path.join(REMIXES_DIR, `${video.id}.json`);
+      removeFileIfExists(remixFilePath);
+    } catch (cleanupErr) {
+      console.warn(`⚠️ 删除 remix 文件失败 (${video.id}):`, cleanupErr.message);
+    }
+
+    if (filename) {
+      try {
+        const baseName = filename.replace(/\.[^.]+$/, '');
+        const whisperOutputDir = path.join(WHISPER_DIR, baseName);
+        removeDirectoryIfExists(whisperOutputDir);
+      } catch (cleanupErr) {
+        console.warn(`⚠️ 删除 Whisper 输出目录失败 (${filename}):`, cleanupErr.message);
+      }
+    }
+
+    writeManifest(manifest);
+
+    console.log(`🗑️ 视频已删除: ${video.title || filename || video.id}`);
+    res.json({ success: true, videoId: video.id, filename });
+  } catch (err) {
+    console.error('❌ DELETE /api/manifest 失败:', err);
+    res.status(500).json({ error: `删除视频失败: ${err.message}` });
+  }
+});
+
+// ==================== 视频上传 ====================
+
+// POST /api/upload/video — 上传视频文件
+app.post('/api/upload/video', upload.single('video'), (req, res) => {
+  try {
+    if (!req.file) {
+      return res.status(400).json({ error: '未收到视频文件' });
+    }
+
+    const file = req.file;
+    const filename = file.filename;
+    const ext = path.extname(filename).replace('.', '').toLowerCase();
+
+    // 生成视频 ID
+    const videoId = `VID-${Date.now()}`;
+
+    // 获取文件大小
+    const fileStat = fs.statSync(file.path);
+
+    // 创建 manifest 条目
+    const videoEntry = {
+      id: videoId,
+      title: req.body.title || path.basename(filename, path.extname(filename)),
+      filename: filename,
+      size: fileStat.size,
+      duration: 0, // 前端可以通过 video 元素获取
+      category: 'uploaded',
+      tags: req.body.tags ? JSON.parse(req.body.tags) : [],
+      description: req.body.description || '用户上传的视频',
+      source: 'uploaded',
+      metadata: {
+        resolution: '未知',
+        format: ext || 'mp4'
+      }
+    };
+
+    // 添加到 manifest
+    const manifest = readManifest();
+    manifest.push(videoEntry);
+    writeManifest(manifest);
+
+    console.log(`📤 视频上传成功: ${filename} (${(fileStat.size / 1024 / 1024).toFixed(1)}MB) → ${videoId}`);
+
+    res.json({
+      success: true,
+      video: videoEntry,
+      filepath: `/backend/video/${filename}`
+    });
+  } catch (err) {
+    console.error('❌ 视频上传失败:', err.message);
+    res.status(500).json({ error: `上传失败: ${err.message}` });
+  }
+});
+
+// 上传错误处理
+app.use((err, req, res, next) => {
+  if (err instanceof multer.MulterError) {
+    if (err.code === 'LIMIT_FILE_SIZE') {
+      return res.status(413).json({ error: '文件大小超过限制(最大 500MB)' });
+    }
+    return res.status(400).json({ error: `上传错误: ${err.message}` });
+  }
+  if (err) {
+    return res.status(400).json({ error: err.message });
+  }
+  next();
+});
+
+app.post('/api/remix/extract-audio', (req, res) => {
+  const { videoId } = req.body || {};
+  if (!videoId) {
+    return res.status(400).json({ error: '缺少 videoId 参数' });
+  }
+
+  const manifest = readManifest();
+  const video = manifest.find(v => v.id === videoId);
+  if (!video) {
+    return res.status(404).json({ error: `未找到视频: ${videoId}` });
+  }
+
+  const videoPath = resolveVideoPath(video.filename);
+  if (!fs.existsSync(videoPath)) {
+    return res.status(404).json({ error: `视频文件不存在: ${video.filename}` });
+  }
+
+  const safeBaseName = path.basename(video.filename, path.extname(video.filename)).replace(/[^a-zA-Z0-9._-]/g, '_') || `audio-${Date.now()}`;
+  const outputFilename = `${safeBaseName}-${Date.now()}.wav`;
+  const outputPath = path.join(DATA_REMIX_ASSET_DIR, outputFilename);
+
+  const ffmpegArgs = ['-y', '-i', videoPath, '-vn', '-acodec', 'pcm_s16le', '-ar', '44100', '-ac', '2', outputPath];
+  const process = spawn('ffmpeg', ffmpegArgs, { cwd: PROJECT_ROOT });
+
+  let stderr = '';
+  process.stderr.on('data', (data) => {
+    stderr += data.toString();
+  });
+
+  process.on('error', (err) => {
+    removeFileIfExists(outputPath);
+    const message = /ENOENT/i.test(err.message)
+      ? '未检测到 ffmpeg,请先安装 ffmpeg 并确保已加入系统 PATH 环境变量'
+      : `无法启动 ffmpeg:${err.message}`;
+    res.status(500).json({ error: message });
+  });
+
+  process.on('close', (code) => {
+    if (code !== 0 || !fs.existsSync(outputPath)) {
+      removeFileIfExists(outputPath);
+      const lastErrorLine = stderr.split('\n').filter(Boolean).slice(-1)[0] || '';
+      const normalizedMessage = /ffmpeg/i.test(stderr) && /not recognized|not found|no such file/i.test(stderr)
+        ? '未检测到 ffmpeg,请先安装 ffmpeg 并确保已加入系统 PATH 环境变量'
+        : `音频提取失败${lastErrorLine ? `: ${lastErrorLine}` : ''}`;
+      return res.status(500).json({ error: normalizedMessage });
+    }
+
+    res.setHeader('Content-Type', 'audio/wav');
+    res.setHeader('Content-Disposition', `attachment; filename="${outputFilename}"`);
+
+    const stream = fs.createReadStream(outputPath);
+    stream.on('close', () => {
+      removeFileIfExists(outputPath);
+    });
+    stream.on('error', () => {
+      removeFileIfExists(outputPath);
+      if (!res.headersSent) {
+        res.status(500).json({ error: '音频文件读取失败' });
+      } else {
+        res.end();
+      }
+    });
+    stream.pipe(res);
+  });
+});
+
+// POST /api/extract-audio-mp3 — 提取视频音轨为 MP3(轻量,适合发给 Gemini 音频识别)
+app.post('/api/extract-audio-mp3', (req, res) => {
+  const { videoId } = req.body || {};
+  if (!videoId) {
+    return res.status(400).json({ error: '缺少 videoId 参数' });
+  }
+
+  const manifest = readManifest();
+  const video = manifest.find(v => v.id === videoId);
+  if (!video) {
+    console.warn(`⚠️ extract-audio-mp3: 未找到 videoId=${videoId}`);
+    return res.status(404).json({ error: `未找到视频: ${videoId}` });
+  }
+
+  const videoPath = resolveVideoPath(video.filename);
+  if (!fs.existsSync(videoPath)) {
+    console.warn(`⚠️ extract-audio-mp3: 视频文件不存在: ${videoPath}`);
+    return res.status(404).json({ error: `视频文件不存在: ${video.filename}` });
+  }
+
+  const safeBaseName = path.basename(video.filename, path.extname(video.filename)).replace(/[^a-zA-Z0-9._-]/g, '_') || `audio-${Date.now()}`;
+  // 使用 AAC(ffmpeg 内置,无需 libmp3lame),封装为 m4a;Gemini 支持 audio/mp4
+  const outputFilename = `${safeBaseName}-${Date.now()}.m4a`;
+  const outputPath = path.join(DATA_REMIX_ASSET_DIR, outputFilename);
+  const outputMimeType = 'audio/mp4';
+
+  console.log(`🎵 开始音频提取: ${videoPath} → ${outputPath}`);
+
+  // 64kbps 单声道 16kHz:体积小且足够语音识别
+  const ffmpegArgs = ['-y', '-i', videoPath, '-vn', '-c:a', 'aac', '-b:a', '64k', '-ar', '16000', '-ac', '1', outputPath];
+  const proc = spawn('ffmpeg', ffmpegArgs, { cwd: PROJECT_ROOT });
+
+  let stderr = '';
+  let responded = false;
+  const safeRespond = (status, body) => {
+    if (responded) return;
+    responded = true;
+    res.status(status).json(body);
+  };
+  proc.stderr.on('data', (data) => { stderr += data.toString(); });
+
+  proc.on('error', (err) => {
+    removeFileIfExists(outputPath);
+    const message = /ENOENT/i.test(err.message)
+      ? '未检测到 ffmpeg,请先安装 ffmpeg 并确保已加入系统 PATH 环境变量'
+      : `无法启动 ffmpeg:${err.message}`;
+    console.error(`❌ extract-audio-mp3 spawn error: ${err.message}`);
+    safeRespond(500, { error: message });
+  });
+
+  proc.on('close', (code) => {
+    if (code !== 0 || !fs.existsSync(outputPath)) {
+      const tail = stderr.split('\n').filter(Boolean).slice(-5).join(' | ');
+      console.error(`❌ ffmpeg exit code=${code} 输出文件存在=${fs.existsSync(outputPath)}`);
+      console.error(`   stderr 末尾: ${tail}`);
+      removeFileIfExists(outputPath);
+      const lastErrorLine = stderr.split('\n').filter(Boolean).slice(-1)[0] || '';
+      return safeRespond(500, { error: `音频提取失败 (exit=${code})${lastErrorLine ? `: ${lastErrorLine}` : ''}` });
+    }
+
+    const stats = fs.statSync(outputPath);
+    const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
+    console.log(`🎵 音频提取完成: ${outputFilename} (${sizeMB}MB)`);
+
+    // 返回 base64 编码的 mp3(方便前端直接发给 Gemini)
+    const audioBuffer = fs.readFileSync(outputPath);
+    const audioBase64 = audioBuffer.toString('base64');
+    removeFileIfExists(outputPath);
+
+    safeRespond(200, {
+      success: true,
+      audio: {
+        base64: audioBase64,
+        mimeType: outputMimeType,
+        sizeMB: parseFloat(sizeMB),
+        filename: outputFilename
+      }
+    });
+  });
+});
+
+app.post('/api/remix/upload-asset', assetUpload.single('file'), async (req, res) => {
+  try {
+    if (!req.file) {
+      return res.status(400).json({ error: '未收到素材文件' });
+    }
+
+    const mimeType = req.file.mimetype || 'application/octet-stream';
+    const kind = mimeType.startsWith('audio/') ? 'audio' : 'image';
+    const key = buildDigitalHumanAssetKey(req.file.originalname, kind);
+    const token = buildQiniuUploadToken(key);
+    const formData = new FormData();
+    formData.append('token', token);
+    formData.append('key', key);
+    formData.append('file', new Blob([req.file.buffer], { type: mimeType }), path.basename(key));
+
+    const response = await fetch(QINIU_UPLOAD_URL, {
+      method: 'POST',
+      body: formData
+    });
+
+    const text = await response.text();
+    let payload = null;
+    try {
+      payload = text ? JSON.parse(text) : null;
+    } catch {
+      payload = null;
+    }
+
+    if (!response.ok) {
+      return res.status(response.status).json({
+        error: payload?.error || payload?.message || text || '七牛素材上传失败',
+        detail: payload || text || ''
+      });
+    }
+
+    const uploadedKey = payload?.key || key;
+    const url = `${QINIU_CDN_DOMAIN.replace(/\/$/, '')}/${uploadedKey}`;
+    if (!uploadedKey || !url) {
+      return res.status(500).json({ error: '七牛未返回素材 Key', detail: payload || text || '' });
+    }
+
+    res.json({
+      success: true,
+      url,
+      key: uploadedKey,
+      mimeType,
+      kind
+    });
+  } catch (error) {
+    console.error('❌ 上传重塑素材失败:', error);
+    res.status(500).json({ error: `上传重塑素材失败: ${error.message}` });
+  }
+});
+
+// ==================== 文件操作 ====================
+
+// GET /api/files/whisper/:videoId — 获取指定视频的 Whisper 输出文件列表
+app.get('/api/files/whisper/:videoId', (req, res) => {
+  try {
+    const manifest = readManifest();
+    const video = manifest.find(v => v.id === req.params.videoId);
+    if (!video) {
+      return res.status(404).json({ error: `未找到视频: ${req.params.videoId}` });
+    }
+
+    const baseName = video.filename.replace(/\.[^.]+$/, '');
+    const outputDir = path.join(WHISPER_DIR, baseName);
+
+    if (!fs.existsSync(outputDir)) {
+      return res.json({ files: [], exists: false });
+    }
+
+    const files = fs.readdirSync(outputDir).map(f => ({
+      name: f,
+      path: `Whisper/${baseName}/${f}`,
+      size: fs.statSync(path.join(outputDir, f)).size
+    }));
+
+    res.json({ files, exists: true });
+  } catch (err) {
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// GET /api/files/read — 读取项目内文件内容
+app.get('/api/files/read', (req, res) => {
+  const filePath = req.query.path;
+  if (!filePath) {
+    return res.status(400).json({ error: '缺少 path 参数' });
+  }
+
+  const fullPath = path.join(PROJECT_ROOT, filePath);
+
+  // 安全检查:不允许读取项目目录外的文件
+  if (!fullPath.startsWith(PROJECT_ROOT)) {
+    return res.status(403).json({ error: '路径不在项目目录内' });
+  }
+
+  if (!fs.existsSync(fullPath)) {
+    return res.status(404).json({ error: '文件不存在' });
+  }
+
+  const ext = path.extname(fullPath).toLowerCase();
+  if (['.json'].includes(ext)) {
+    res.json(JSON.parse(fs.readFileSync(fullPath, 'utf-8')));
+  } else {
+    res.type('text/plain').send(fs.readFileSync(fullPath, 'utf-8'));
+  }
+});
+
+app.post('/api/download/video', (req, res) => {
+  const {
+    url,
+    urls,
+    filename,
+    title,
+    description,
+    tags,
+    thumbnail,
+    duration,
+    resolution,
+    awemeId,
+    authorName
+  } = req.body || {};
+
+  if (!url || typeof url !== 'string') {
+    return res.status(400).json({ error: '缺少 url 参数' });
+  }
+
+  const candidateUrls = normalizeRemoteUrls(url, urls);
+  if (candidateUrls.length === 0) {
+    return res.status(400).json({ error: '无效的视频地址' });
+  }
+
+  const safeFilename = ensureUniqueVideoFilename(ensureVideoFilename(filename || title || awemeId || 'douyin-video.mp4', url));
+  const filePath = path.join(DATA_VIDEO_DIR, safeFilename);
+  const taskId = `DL-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
+  const task = {
+    id: taskId,
+    status: 'pending',
+    progress: 0,
+    downloadedBytes: 0,
+    totalBytes: 0,
+    filename: safeFilename,
+    created_at: new Date().toISOString()
+  };
+
+  downloadTasks.set(taskId, task);
+  res.json({ success: true, taskId, filename: safeFilename });
+
+  (async () => {
+    try {
+      task.status = 'downloading';
+      console.log(`📥 开始下载远程视频: ${safeFilename} ← ${candidateUrls[0]}`);
+
+      const { url: resolvedUrl, response } = await fetchRemoteVideoResponse(candidateUrls, {
+        'Accept': '*/*',
+        'User-Agent': 'Mozilla/5.0',
+        'Referer': 'https://www.douyin.com/',
+        'Origin': 'https://www.douyin.com'
+      });
+      task.sourceUrl = resolvedUrl;
+
+      const totalBytes = Number.parseInt(response.headers.get('content-length') || '0', 10) || 0;
+      task.totalBytes = totalBytes;
+
+      let downloadedBytes = 0;
+      let chunkCount = 0;
+
+      const progressStream = new Transform({
+        transform(chunk, encoding, callback) {
+          downloadedBytes += chunk.length;
+          chunkCount += 1;
+          task.downloadedBytes = downloadedBytes;
+          task.progress = totalBytes > 0
+            ? Math.min(99, Math.round((downloadedBytes / totalBytes) * 100))
+            : Math.min(95, Math.max(task.progress || 0, Math.min(95, chunkCount)));
+          callback(null, chunk);
+        }
+      });
+
+      await pipeline(
+        Readable.fromWeb(response.body),
+        progressStream,
+        fs.createWriteStream(filePath)
+      );
+
+      const stat = fs.statSync(filePath);
+      const manifest = readManifest();
+      const videoEntry = createManagedVideoEntry({
+        filename: safeFilename,
+        title,
+        description,
+        tags,
+        thumbnail,
+        duration,
+        resolution,
+        awemeId,
+        authorName,
+        size: stat.size
+      });
+
+      manifest.push(videoEntry);
+      writeManifest(manifest);
+
+      task.status = 'completed';
+      task.progress = 100;
+      task.completed_at = new Date().toISOString();
+      task.video = videoEntry;
+
+      console.log(`✅ 远程视频下载完成: ${safeFilename} (${(stat.size / 1024 / 1024).toFixed(1)}MB)`);
+    } catch (error) {
+      removeFileIfExists(filePath);
+      task.status = 'failed';
+      task.error = error.message;
+      task.failed_at = new Date().toISOString();
+      console.error('❌ 远程视频下载失败:', error);
+    }
+  })();
+});
+
+app.get('/api/download/video/:taskId', (req, res) => {
+  const task = downloadTasks.get(req.params.taskId);
+  if (!task) {
+    return res.status(404).json({ error: '未找到下载任务' });
+  }
+
+  res.json(task);
+});
+
+// ==================== 视频流代理 ====================
+
+app.get('/api/video-proxy', async (req, res) => {
+  const { url, filename = 'douyin-video.mp4', download } = req.query;
+
+  if (!url || typeof url !== 'string') {
+    return res.status(400).json({ error: '缺少 url 参数' });
+  }
+
+  if (!isSafeRemoteUrl(url)) {
+    return res.status(400).json({ error: '无效的视频地址' });
+  }
+
+  try {
+    const upstreamHeaders = {
+      'Accept': req.headers.accept || '*/*',
+      'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0',
+      'Referer': 'https://www.douyin.com/',
+      'Origin': 'https://www.douyin.com'
+    };
+
+    if (req.headers.range) {
+      upstreamHeaders.Range = req.headers.range;
+    }
+
+    const response = await fetch(url, {
+      method: 'GET',
+      headers: upstreamHeaders,
+      redirect: 'follow'
+    });
+
+    if (!response.ok && response.status !== 206) {
+      const errorText = await response.text().catch(() => '');
+      return res.status(response.status).json({
+        error: '远程视频请求失败',
+        status: response.status,
+        detail: errorText
+      });
+    }
+
+    res.status(response.status);
+
+    const passthroughHeaders = [
+      'content-type',
+      'content-length',
+      'content-range',
+      'accept-ranges',
+      'etag',
+      'last-modified',
+      'cache-control'
+    ];
+
+    passthroughHeaders.forEach((headerName) => {
+      const value = response.headers.get(headerName);
+      if (value) {
+        res.setHeader(headerName, value);
+      }
+    });
+
+    if (!response.headers.get('content-type')) {
+      res.setHeader('Content-Type', 'video/mp4');
+    }
+
+    res.setHeader(
+      'Content-Disposition',
+      download === '1'
+        ? `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`
+        : `inline; filename*=UTF-8''${encodeURIComponent(filename)}`
+    );
+
+    if (!response.body) {
+      return res.end();
+    }
+
+    Readable.fromWeb(response.body).pipe(res);
+  } catch (error) {
+    console.error('视频代理失败:', error);
+    res.status(500).json({ error: `视频代理失败: ${error.message}` });
+  }
+});
+
+// ==================== AI 重塑记录 ====================
+
+const REMIXES_DIR = path.join(DATA_DIR, 'remixes');
+if (!fs.existsSync(REMIXES_DIR)) fs.mkdirSync(REMIXES_DIR, { recursive: true });
+
+function readRemixes(videoId) {
+  const p = path.join(REMIXES_DIR, `${videoId}.json`);
+  if (!fs.existsSync(p)) return [];
+  return JSON.parse(fs.readFileSync(p, 'utf-8'));
+}
+
+function writeRemixes(videoId, data) {
+  fs.writeFileSync(path.join(REMIXES_DIR, `${videoId}.json`), JSON.stringify(data, null, 2), 'utf-8');
+}
+
+// GET /api/remixes/:videoId — 获取某视频的所有重塑记录
+app.get('/api/remixes/:videoId', (req, res) => {
+  res.json(readRemixes(req.params.videoId));
+});
+
+// POST /api/remixes/:videoId — 创建或更新一条重塑记录
+// body: { remixId, ...remixData }
+app.post('/api/remixes/:videoId', (req, res) => {
+  const remixes = readRemixes(req.params.videoId);
+  const { remixId } = req.body;
+
+  const idx = remixes.findIndex(r => r.remixId === remixId);
+  if (idx >= 0) {
+    // 更新已有记录(合并 segments)
+    remixes[idx] = { ...remixes[idx], ...req.body, updated_at: new Date().toISOString() };
+  } else {
+    // 新增记录
+    remixes.unshift({ ...req.body, created_at: new Date().toISOString(), updated_at: new Date().toISOString() });
+  }
+
+  writeRemixes(req.params.videoId, remixes);
+  res.json({ success: true });
+});
+
+// DELETE /api/remixes/:videoId/:remixId — 删除某条重塑记录
+app.delete('/api/remixes/:videoId/:remixId', (req, res) => {
+  let remixes = readRemixes(req.params.videoId);
+  remixes = remixes.filter(r => r.remixId !== req.params.remixId);
+  writeRemixes(req.params.videoId, remixes);
+  res.json({ success: true });
+});
+
+// GET /api/remixes — 获取所有视频的重塑记录汇总
+app.get('/api/remixes', (req, res) => {
+  const files = fs.readdirSync(REMIXES_DIR).filter(f => f.endsWith('.json'));
+  const all = {};
+  files.forEach(f => {
+    const videoId = f.replace('.json', '');
+    all[videoId] = JSON.parse(fs.readFileSync(path.join(REMIXES_DIR, f), 'utf-8'));
+  });
+  res.json(all);
+});
+
+// ==================== 一键成片(Quickly 代理) ====================
+
+const QUICKLY_APP_KEY = 'ZmNmOGRhNjYzZTAx';
+const QUICKLY_APP_SECRET = 'eaa12154c248cad9159a9d6ea8bedf46';
+const QUICKLY_ACCOUNT_ID = '12859_117409';
+const QUICKLY_CALLBACK_URL = 'https://server.fmode.cn/api/functions/cut/onemerge';
+const QUICKLY_RELAY_URL = 'https://server.fmode.cn/api/functions';
+
+// POST /api/quickly/create — 代理一键成片请求(服务端签名)
+app.post('/api/quickly/create', async (req, res) => {
+  try {
+    const { videoUrls, options = {} } = req.body;
+    if (!videoUrls || !Array.isArray(videoUrls) || videoUrls.length === 0) {
+      return res.status(400).json({ error: '缺少 videoUrls 参数' });
+    }
+
+    const timestamp = Date.now().toString();
+    const signStr = timestamp + '#' + QUICKLY_APP_SECRET;
+    const sign = crypto.createHash('md5').update(signStr).digest('hex');
+
+    const materialList = videoUrls.map(url => ({ type: 'video', value: url }));
+    const preId = timestamp;
+
+    const apiBody = {
+      account_id: QUICKLY_ACCOUNT_ID,
+      callback_url: QUICKLY_CALLBACK_URL,
+      material_list: materialList,
+      tags: options.tags || '视频,AI生成',
+      proportion: options.proportion || '9:16',
+      video_duration: options.videoDuration || { min: 10, max: 30 },
+      pre_id: preId,
+      compose_number: 1,
+      ai_voice: options.aiVoice ?? 1,
+      ai_bgm: options.aiBgm ?? 1,
+      ai_flower: 1,
+      ai_subtitle: options.aiSubtitle ?? 0,
+      original_voice: 0
+    };
+
+    const relayData = JSON.stringify({
+      apiPath: '/v2/video/vlog/create',
+      apiBody: apiBody,
+      appKey: QUICKLY_APP_KEY,
+      timestamp: timestamp,
+      sign: sign
+    });
+
+    const body = JSON.stringify({ action: 'relay', relayData: relayData });
+
+    console.log(`📦 一键成片 - timestamp=${timestamp}, sign=${sign}`);
+    console.log(`   材料: ${videoUrls.length} 个视频`);
+
+    // 使用 Node 原生 fetch 发送请求
+    const response = await fetch(QUICKLY_RELAY_URL, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: body
+    });
+
+    const result = await response.json();
+    console.log('📦 一键成片 - 响应:', JSON.stringify(result).substring(0, 300));
+
+    res.json(result);
+  } catch (err) {
+    console.error('❌ 一键成片代理失败:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// POST /api/quickly/query — 代理查询一键成片结果(通过 Parse 云函数)
+app.post('/api/quickly/query', async (req, res) => {
+  try {
+    const { taskId } = req.body;
+    if (!taskId) return res.status(400).json({ error: '缺少 taskId 参数' });
+
+    const body = JSON.stringify({
+      id: 'sWvRr8RvPT',
+      _ApplicationId: 'ncloudmaster',
+      action: 'query',
+      taskId: taskId
+    });
+
+    const QUICKLY_QUERY_URL = 'https://server.fmode.cn/api/functions';
+    const response = await fetch(QUICKLY_QUERY_URL, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: body
+    });
+
+    const result = await response.json();
+    res.json(result);
+  } catch (err) {
+    console.error('❌ 一键成片查询失败:', err.message);
+    res.status(500).json({ error: err.message });
+  }
+});
+
+// ==================== 视频合成(图片+音频 → 视频)====================
+
+const COMPOSITE_DIR = path.join(DATA_DIR, 'composite');
+if (!fs.existsSync(COMPOSITE_DIR)) fs.mkdirSync(COMPOSITE_DIR, { recursive: true });
+
+// 下载远程文件到本地
+async function downloadFile(url, destPath) {
+  const response = await fetch(url, { redirect: 'follow' });
+  if (!response.ok) throw new Error(`下载失败 (${response.status}): ${url}`);
+  const buffer = Buffer.from(await response.arrayBuffer());
+  fs.writeFileSync(destPath, buffer);
+  return destPath;
+}
+
+// 获取音频时长(秒)
+function getAudioDuration(audioPath) {
+  return new Promise((resolve, reject) => {
+    const proc = spawn('ffprobe', [
+      '-v', 'error', '-show_entries', 'format=duration',
+      '-of', 'default=noprint_wrappers=1:nokey=1', audioPath
+    ]);
+    let stdout = '';
+    proc.stdout.on('data', d => stdout += d.toString());
+    proc.on('error', reject);
+    proc.on('close', code => {
+      const dur = parseFloat(stdout.trim());
+      if (code !== 0 || isNaN(dur)) reject(new Error('无法获取音频时长'));
+      else resolve(dur);
+    });
+  });
+}
+
+// 将图片转为 jpg(确保 ffmpeg 兼容性)
+function convertImageToJpg(inputPath, outputPath) {
+  return new Promise((resolve, reject) => {
+    const proc = spawn('ffmpeg', ['-y', '-i', inputPath, '-frames:v', '1', outputPath]);
+    let stderr = '';
+    proc.stderr.on('data', d => stderr += d.toString());
+    proc.on('error', reject);
+    proc.on('close', code => {
+      if (code !== 0) reject(new Error(`图片转换失败: ${stderr.split('\n').filter(Boolean).slice(-1)[0]}`));
+      else resolve(outputPath);
+    });
+  });
+}
+
+// 单个片段:图片 + 音频 → 视频
+function createSegmentVideo(imagePath, audioPath, outputPath, duration) {
+  return new Promise(async (resolve, reject) => {
+    try {
+      // 先将图片统一转为 jpg 确保兼容性
+      const jpgPath = imagePath.replace(/\.[^.]+$/, '') + '_converted.jpg';
+      await convertImageToJpg(imagePath, jpgPath);
+
+      const args = [
+        '-y',
+        '-loop', '1', '-i', jpgPath,
+        '-i', audioPath,
+        '-c:v', 'h264_mf',
+        '-c:a', 'aac', '-b:a', '192k',
+        '-vf', 'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black',
+        '-pix_fmt', 'yuv420p',
+        '-t', String(duration),
+        '-shortest',
+        outputPath
+      ];
+      const proc = spawn('ffmpeg', args);
+      let stderr = '';
+      proc.stderr.on('data', d => stderr += d.toString());
+      proc.on('error', err => reject(new Error(/ENOENT/i.test(err.message) ? '未检测到 ffmpeg' : err.message)));
+      proc.on('close', code => {
+        removeFileIfExists(jpgPath);
+        if (code !== 0) reject(new Error(`片段合成失败: ${stderr.split('\n').filter(Boolean).slice(-2).join(' ')}`));
+        else resolve(outputPath);
+      });
+    } catch (err) {
+      reject(err);
+    }
+  });
+}
+
+// 拼接多个视频片段
+function concatVideos(segmentPaths, outputPath) {
+  return new Promise((resolve, reject) => {
+    // 创建 concat 文件列表
+    const listPath = outputPath + '.txt';
+    const listContent = segmentPaths.map(p => `file '${p.replace(/\\/g, '/')}'`).join('\n');
+    fs.writeFileSync(listPath, listContent, 'utf-8');
+
+    const args = [
+      '-y', '-f', 'concat', '-safe', '0',
+      '-i', listPath,
+      '-c', 'copy',
+      outputPath
+    ];
+    const proc = spawn('ffmpeg', args);
+    let stderr = '';
+    proc.stderr.on('data', d => stderr += d.toString());
+    proc.on('error', err => reject(new Error(/ENOENT/i.test(err.message) ? '未检测到 ffmpeg' : err.message)));
+    proc.on('close', code => {
+      removeFileIfExists(listPath);
+      if (code !== 0) reject(new Error(`视频拼接失败: ${stderr.split('\n').filter(Boolean).slice(-2).join(' ')}`));
+      else resolve(outputPath);
+    });
+  });
+}
+
+// POST /api/video/composite — 合成视频(图片+音频 → 最终视频)
+app.post('/api/video/composite', async (req, res) => {
+  const { segments, title } = req.body;
+  // segments: [{ imageUrl, audioUrl, id }]
+  if (!Array.isArray(segments) || segments.length === 0) {
+    return res.status(400).json({ error: '缺少 segments 参数' });
+  }
+
+  const jobId = `VG-${Date.now()}`;
+  const jobDir = path.join(COMPOSITE_DIR, jobId);
+  fs.mkdirSync(jobDir, { recursive: true });
+
+  try {
+    console.log(`🎬 开始合成视频: ${jobId}, ${segments.length} 个片段`);
+
+    // 1. 下载所有素材
+    const segmentPaths = [];
+    for (let i = 0; i < segments.length; i++) {
+      const seg = segments[i];
+      if (!seg.imageUrl || !seg.audioUrl) {
+        console.warn(`⚠️ 片段 ${seg.id || i} 缺少素材,跳过`);
+        continue;
+      }
+
+      const imgExt = (seg.imageUrl.match(/\.(jpg|jpeg|png|webp|gif)/i) || ['.jpg'])[0] || '.jpg';
+      const imgPath = path.join(jobDir, `img-${i}${imgExt}`);
+      const audioExt = (seg.audioUrl.match(/\.(mp3|wav|aac|ogg|m4a)/i) || ['.mp3'])[0] || '.mp3';
+      const audioPath = path.join(jobDir, `audio-${i}${audioExt}`);
+      const segVideoPath = path.join(jobDir, `seg-${i}.mp4`);
+
+      console.log(`  📥 下载片段 ${i + 1}/${segments.length}...`);
+      await downloadFile(seg.imageUrl, imgPath);
+      await downloadFile(seg.audioUrl, audioPath);
+
+      // 获取音频时长
+      const duration = await getAudioDuration(audioPath);
+      console.log(`  🎞️ 合成片段 ${i + 1}/${segments.length} (${duration.toFixed(1)}s)...`);
+
+      await createSegmentVideo(imgPath, audioPath, segVideoPath, duration);
+      segmentPaths.push(segVideoPath);
+    }
+
+    if (segmentPaths.length === 0) {
+      return res.status(400).json({ error: '没有有效的素材片段' });
+    }
+
+    // 2. 拼接所有片段
+    const safeTitle = String(title || 'video').replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, '_').substring(0, 50);
+    const finalFilename = `${safeTitle}-${jobId}.mp4`;
+    const finalPath = path.join(COMPOSITE_DIR, finalFilename);
+
+    console.log(`  🔗 拼接 ${segmentPaths.length} 个片段...`);
+
+    if (segmentPaths.length === 1) {
+      // 只有一个片段,直接复制
+      fs.copyFileSync(segmentPaths[0], finalPath);
+    } else {
+      await concatVideos(segmentPaths, finalPath);
+    }
+
+    // 3. 清理临时文件
+    try {
+      fs.rmSync(jobDir, { recursive: true, force: true });
+    } catch {}
+
+    const fileSize = fs.statSync(finalPath).size;
+    console.log(`✅ 视频合成完成: ${finalFilename} (${(fileSize / 1024 / 1024).toFixed(1)}MB)`);
+
+    res.json({
+      success: true,
+      videoUrl: `/api/video/composite/${finalFilename}`,
+      filename: finalFilename,
+      size: fileSize,
+      segments: segmentPaths.length
+    });
+
+  } catch (err) {
+    console.error(`❌ 视频合成失败:`, err.message);
+    // 清理
+    try { fs.rmSync(jobDir, { recursive: true, force: true }); } catch {}
+    res.status(500).json({ error: `视频合成失败: ${err.message}` });
+  }
+});
+
+// GET /api/video/composite/:filename — 下载合成视频
+app.get('/api/video/composite/:filename', (req, res) => {
+  const filePath = path.join(COMPOSITE_DIR, req.params.filename);
+  if (!fs.existsSync(filePath)) {
+    return res.status(404).json({ error: '视频文件不存在' });
+  }
+  res.setHeader('Content-Type', 'video/mp4');
+  fs.createReadStream(filePath).pipe(res);
+});
+
+// ==================== 任务管理 ====================
+
+// GET /api/tasks — 获取所有任务
+app.get('/api/tasks', (req, res) => {
+  res.json(readDataFile('tasks'));
+});
+
+// POST /api/tasks — 创建新任务
+app.post('/api/tasks', (req, res) => {
+  const tasks = readDataFile('tasks');
+  const task = { ...req.body, created_at: new Date().toISOString(), updated_at: new Date().toISOString() };
+  // 确保始终有有效 ID(即使前端传了空 ID)
+  if (!task.id) task.id = `TASK-${Date.now()}`;
+  tasks.unshift(task);
+  writeDataFile('tasks', tasks);
+  res.json({ success: true, task });
+});
+
+// PUT /api/tasks/:id — 更新任务
+app.put('/api/tasks/:id', (req, res) => {
+  const tasks = readDataFile('tasks');
+  const idx = tasks.findIndex(t => t.id === req.params.id);
+  if (idx === -1) return res.status(404).json({ error: `任务不存在: ${req.params.id}` });
+  tasks[idx] = { ...tasks[idx], ...req.body, updated_at: new Date().toISOString() };
+  writeDataFile('tasks', tasks);
+  res.json({ success: true, task: tasks[idx] });
+});
+
+// DELETE /api/tasks/:id — 删除任务
+app.delete('/api/tasks/:id', (req, res) => {
+  let tasks = readDataFile('tasks');
+  const len = tasks.length;
+  tasks = tasks.filter(t => t.id !== req.params.id);
+  if (tasks.length === len) return res.status(404).json({ error: `任务不存在: ${req.params.id}` });
+  writeDataFile('tasks', tasks);
+  res.json({ success: true });
+});
+
+// ==================== 生成历史 ====================
+
+// GET /api/history — 获取历史记录
+app.get('/api/history', (req, res) => {
+  res.json(readDataFile('history'));
+});
+
+// POST /api/history — 添加历史记录
+app.post('/api/history', (req, res) => {
+  const history = readDataFile('history');
+  const record = { id: `HIS-${Date.now()}`, created_at: new Date().toISOString(), ...req.body };
+  history.unshift(record);
+  writeDataFile('history', history);
+  res.json({ success: true, record });
+});
+
+// DELETE /api/history/:id — 删除历史记录
+app.delete('/api/history/:id', (req, res) => {
+  let history = readDataFile('history');
+  const len = history.length;
+  history = history.filter(h => h.id !== req.params.id);
+  if (history.length === len) return res.status(404).json({ error: `记录不存在: ${req.params.id}` });
+  writeDataFile('history', history);
+  res.json({ success: true });
+});
+
+// DELETE /api/history — 清空所有历史
+app.delete('/api/history', (req, res) => {
+  writeDataFile('history', []);
+  res.json({ success: true });
+});
+
+// ==================== 生成结果 ====================
+
+// GET /api/results — 获取所有结果
+app.get('/api/results', (req, res) => {
+  res.json(readDataFile('results'));
+});
+
+// POST /api/results — 添加结果
+app.post('/api/results', (req, res) => {
+  const results = readDataFile('results');
+  const result = { id: `RES-${Date.now()}`, created_at: new Date().toISOString(), ...req.body };
+  results.unshift(result);
+  writeDataFile('results', results);
+  res.json({ success: true, result });
+});
+
+// PUT /api/results/:id — 更新结果
+app.put('/api/results/:id', (req, res) => {
+  const results = readDataFile('results');
+  const idx = results.findIndex(r => r.id === req.params.id);
+  if (idx === -1) return res.status(404).json({ error: `结果不存在: ${req.params.id}` });
+  results[idx] = { ...results[idx], ...req.body };
+  writeDataFile('results', results);
+  res.json({ success: true, result: results[idx] });
+});
+
+// DELETE /api/results/:id — 删除结果
+app.delete('/api/results/:id', (req, res) => {
+  let results = readDataFile('results');
+  const len = results.length;
+  results = results.filter(r => r.id !== req.params.id);
+  if (results.length === len) return res.status(404).json({ error: `结果不存在: ${req.params.id}` });
+  writeDataFile('results', results);
+  res.json({ success: true });
+});
+
+// ==================== LLM 大模型代理 ====================
+
+const LLM_BASE_URL = 'http://server.fmode.cn:9999';
+const LLM_API_KEY = 'sk-MFBOnsAtZiqlwwMgMLKCFmPy55pMohQEGMqsIw3aJrIgvoEO';
+
+// POST /api/llm/chat — OpenAI ChatCompletions 代理
+app.post('/api/llm/chat', async (req, res) => {
+  try {
+    const { model, messages, temperature, max_tokens, stream, ...rest } = req.body;
+    if (!messages || !Array.isArray(messages)) {
+      return res.status(400).json({ error: '缺少 messages 参数' });
+    }
+
+    const payload = {
+      model: model || 'gpt-4o-mini',
+      messages,
+      temperature: temperature ?? 0.7,
+      max_tokens: max_tokens || 4096,
+      stream: stream || false,
+      ...rest
+    };
+
+    const url = `${LLM_BASE_URL}/v1/chat/completions`;
+    console.log(`🤖 LLM Chat 请求: model=${payload.model}, messages=${messages.length}条`);
+
+    if (payload.stream) {
+      // 流式响应
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'Authorization': `Bearer ${LLM_API_KEY}`
+        },
+        body: JSON.stringify(payload)
+      });
+
+      if (!response.ok) {
+        const errText = await response.text();
+        console.error('🤖 LLM Stream 错误:', response.status, errText);
+        return res.status(response.status).json({ error: errText });
+      }
+
+      res.setHeader('Content-Type', 'text/event-stream');
+      res.setHeader('Cache-Control', 'no-cache');
+      res.setHeader('Connection', 'keep-alive');
+
+      const reader = response.body;
+      reader.on('data', (chunk) => res.write(chunk));
+      reader.on('end', () => res.end());
+      reader.on('error', (err) => {
+        console.error('🤖 LLM Stream 读取错误:', err.message);
+        res.end();
+      });
+    } else {
+      // 非流式响应
+      const response = await fetch(url, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          'Authorization': `Bearer ${LLM_API_KEY}`
+        },
+        body: JSON.stringify(payload)
+      });
+
+      const data = await response.json();
+      if (!response.ok) {
+        console.error('🤖 LLM Chat 错误:', response.status, data);
+        return res.status(response.status).json(data);
+      }
+
+      console.log(`🤖 LLM Chat 完成: tokens=${data?.usage?.total_tokens || '?'}`);
+      res.json(data);
+    }
+  } catch (err) {
+    console.error('🤖 LLM Chat 异常:', err.message);
+    res.status(500).json({ error: `LLM 请求失败: ${err.message}` });
+  }
+});
+
+// POST /api/llm/gemini — Gemini 原生格式代理(支持媒体识别)
+app.post('/api/llm/gemini', async (req, res) => {
+  try {
+    const { model, contents, generationConfig, safetySettings, systemInstruction } = req.body;
+    if (!contents) {
+      return res.status(400).json({ error: '缺少 contents 参数' });
+    }
+
+    const geminiModel = model || 'gemini-2.5-flash';
+    const url = `${LLM_BASE_URL}/v1beta/models/${geminiModel}:generateContent`;
+
+    const payload = { contents };
+    if (generationConfig) payload.generationConfig = generationConfig;
+    if (safetySettings) payload.safetySettings = safetySettings;
+    if (systemInstruction) payload.systemInstruction = systemInstruction;
+
+    console.log(`🤖 Gemini 请求: model=${geminiModel}, parts=${contents?.[0]?.parts?.length || 0}`);
+
+    const response = await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': `Bearer ${LLM_API_KEY}`
+      },
+      body: JSON.stringify(payload)
+    });
+
+    const data = await response.json();
+    if (!response.ok) {
+      console.error('🤖 Gemini 错误:', response.status, data);
+      return res.status(response.status).json(data);
+    }
+
+    console.log(`🤖 Gemini 完成: tokens=${data?.usageMetadata?.totalTokenCount || '?'}`);
+    res.json(data);
+  } catch (err) {
+    console.error('🤖 Gemini 异常:', err.message);
+    res.status(500).json({ error: `Gemini 请求失败: ${err.message}` });
+  }
+});
+
+// ==================== 健康检查 ====================
+
+app.get('/api/health', (req, res) => {
+  res.json({
+    status: 'ok',
+    timestamp: new Date().toISOString(),
+    project: PROJECT_ROOT,
+    services: {
+      manifest: fs.existsSync(MANIFEST_PATH),
+      whisperDir: fs.existsSync(WHISPER_DIR),
+      videoDir: fs.existsSync(DATA_VIDEO_DIR) || fs.existsSync(LEGACY_VIDEO_DIR)
+    }
+  });
+});
+
+// ==================== 启动 ====================
+
+app.listen(PORT, () => {
+  console.log('');
+  console.log('========================================');
+  console.log(`  🚀 后端服务已启动: http://localhost:${PORT}`);
+  console.log(`  📁 项目根目录: ${PROJECT_ROOT}`);
+  console.log(`  📋 接口列表:`);
+  console.log(`     GET  /api/health              — 健康检查`);
+  console.log(`     GET  /api/whisper/status       — Whisper 可用性`);
+  console.log(`     POST /api/whisper/transcribe   — 语音转文字`);
+  console.log(`     GET  /api/manifest             — 获取视频清单`);
+  console.log(`     PUT  /api/manifest/:videoId    — 更新视频信息`);
+  console.log(`     POST /api/manifest             — 添加视频条目`);
+  console.log(`     GET  /api/files/whisper/:id    — Whisper 输出文件`);
+  console.log(`     GET  /api/files/read?path=     — 读取项目文件`);
+  console.log(`     POST /api/upload/video         — 上传视频文件`);
+  console.log(`     POST /api/remix/extract-audio  — 提取视频音频`);
+  console.log(`     POST /api/remix/upload-asset   — 上传重塑素材到 Parse`);
+  console.log(`     POST /api/download/video       — 下载远程视频到本地库`);
+  console.log(`     GET  /api/download/video/:id   — 查询下载任务进度`);
+  console.log(`     ---`);
+  console.log(`     GET  /api/tasks               — 获取任务列表`);
+  console.log(`     POST /api/tasks               — 创建任务`);
+  console.log(`     PUT  /api/tasks/:id            — 更新任务`);
+  console.log(`     DEL  /api/tasks/:id            — 删除任务`);
+  console.log(`     ---`);
+  console.log(`     GET  /api/history              — 获取历史记录`);
+  console.log(`     POST /api/history              — 添加历史`);
+  console.log(`     DEL  /api/history/:id           — 删除历史`);
+  console.log(`     DEL  /api/history               — 清空历史`);
+  console.log(`     ---`);
+  console.log(`     GET  /api/results              — 获取结果`);
+  console.log(`     POST /api/results              — 添加结果`);
+  console.log(`     PUT  /api/results/:id          — 更新结果`);
+  console.log(`     DEL  /api/results/:id           — 删除结果`);
+  console.log(`     ---`);
+  console.log(`     POST /api/llm/chat             — LLM 对话(OpenAI格式)`);
+  console.log(`     POST /api/llm/gemini           — Gemini 媒体识别`);
+  console.log('========================================');
+  console.log('');
+});

+ 13 - 0
src/app/app.config.ts

@@ -0,0 +1,13 @@
+import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
+import { provideRouter } from '@angular/router';
+import { provideHttpClient, withFetch } from '@angular/common/http';
+
+import { routes } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+  providers: [
+    provideBrowserGlobalErrorListeners(),
+    provideRouter(routes),
+    provideHttpClient(withFetch())
+  ]
+};

+ 7479 - 0
src/app/app.css

@@ -0,0 +1,7479 @@
+/* 抖音AI视频生成系统 - 样式文件 */
+
+/* 全局变量 */
+:root {
+  --primary-color: #0071e3;
+  --primary-dark: #0066cc;
+  --accent-color: #2997ff;
+  --accent-dark: #0077ed;
+  --background-color: #f5f5f7;
+  --surface-color: #fbfbfd;
+  --surface-elevated: #ffffff;
+  --surface-dark: #1d1d1f;
+  --surface-dark-soft: #272729;
+  --text-primary: #1d1d1f;
+  --text-secondary: rgba(29, 29, 31, 0.72);
+  --text-muted: rgba(29, 29, 31, 0.48);
+  --border-color: rgba(29, 29, 31, 0.08);
+  --success-color: #48bb78;
+  --warning-color: #ed8936;
+  --error-color: #f56565;
+  --info-color: #0071e3;
+  --shadow-sm: 0 10px 30px rgba(0, 0, 0, 0.08);
+  --shadow-md: 0 18px 50px rgba(0, 0, 0, 0.12);
+  --shadow-lg: 3px 5px 30px rgba(0, 0, 0, 0.22);
+  --border-radius-sm: 8px;
+  --border-radius-md: 12px;
+  --border-radius-lg: 12px;
+  --pill-radius: 980px;
+  --transition-fast: 0.15s ease-in-out;
+  --transition-normal: 0.3s ease-in-out;
+}
+
+:host {
+  display: block;
+  color: var(--text-primary);
+}
+
+* {
+  box-sizing: border-box;
+}
+
+/* 图标样式 */
+.icon {
+  display: none;
+  align-items: center;
+  justify-content: center;
+  width: 24px;
+  height: 24px;
+  font-size: 18px;
+  line-height: 1;
+  user-select: none;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.icon.large {
+  width: 64px;
+  height: 64px;
+  font-size: 48px;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+.nav-icon {
+  display: none;
+  align-items: center;
+  justify-content: center;
+  width: 24px;
+  height: 24px;
+  font-size: 18px;
+  line-height: 1;
+  user-select: none;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+/* 按钮样式 */
+.btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 10px 18px;
+  border: 1px solid transparent;
+  border-radius: var(--pill-radius);
+  font-size: 15px;
+  line-height: 1.2;
+  font-weight: 400;
+  cursor: pointer;
+  transition: background-color var(--transition-fast), color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
+  text-decoration: none;
+  background: rgba(255, 255, 255, 0.72);
+  color: var(--text-primary);
+  min-height: 40px;
+}
+
+.btn:hover {
+  background-color: rgba(29, 29, 31, 0.06);
+  transform: translateY(-1px);
+}
+
+.btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+  transform: none;
+}
+
+.btn:focus-visible,
+.form-field input:focus-visible,
+.form-field select:focus-visible,
+.icon-btn:focus-visible,
+.preview-close-btn:focus-visible {
+  outline: 2px solid var(--primary-color);
+  outline-offset: 2px;
+}
+
+.btn-primary {
+  background: var(--primary-color);
+  color: white;
+  box-shadow: 0 8px 24px rgba(0, 113, 227, 0.28);
+}
+
+.btn-primary:hover {
+  background: var(--accent-color);
+  color: white;
+}
+
+.btn-accent {
+  background: var(--surface-dark);
+  color: white;
+}
+
+.btn-accent:hover {
+  background: #2c2c2e;
+  color: white;
+}
+
+.btn-warn {
+  background-color: #b42318;
+  color: white;
+}
+
+.btn-warn:hover {
+  background-color: #981b1c;
+  color: white;
+}
+
+/* 表单字段样式 */
+.form-field {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.form-field label {
+  font-size: 12px;
+  font-weight: 600;
+  letter-spacing: -0.12px;
+  color: var(--text-secondary);
+}
+
+.form-field input,
+.form-field select {
+  padding: 12px 16px;
+  border: 1px solid var(--border-color);
+  border-radius: 11px;
+  font-size: 15px;
+  line-height: 1.47;
+  color: var(--text-primary);
+  background-color: rgba(255, 255, 255, 0.88);
+  transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast);
+}
+
+.form-field input:focus,
+.form-field select:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.12);
+  background-color: #ffffff;
+}
+
+/* 状态标识样式 */
+.status-badge {
+  padding: 4px 12px;
+  border-radius: var(--border-radius-sm);
+  font-size: 12px;
+  font-weight: 600;
+  text-transform: uppercase;
+}
+
+.status-badge.primary {
+  background-color: var(--primary-color);
+  color: white;
+}
+
+.status-badge.accent {
+  background-color: var(--accent-color);
+  color: white;
+}
+
+.status-badge.warn {
+  background-color: var(--error-color);
+  color: white;
+}
+
+.status-badge.success {
+  background-color: var(--success-color);
+  color: white;
+}
+
+/* 基础布局 */
+.app-container {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background-color: var(--background-color);
+  font-family: 'Inter', 'SF Pro Text', 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+}
+
+/* 头部导航 */
+.header {
+  background: rgba(0, 0, 0, 0.8) !important;
+  color: white;
+  padding: 0 24px;
+  height: 52px;
+  display: flex;
+  align-items: center;
+  position: sticky;
+  top: 0;
+  z-index: 1000;
+  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+  backdrop-filter: saturate(180%) blur(20px);
+}
+
+.header .logo {
+  font-size: 18px;
+  font-weight: 600;
+  letter-spacing: -0.02em;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.header .logo small {
+  font-size: 12px;
+  opacity: 0.8;
+  font-weight: 400;
+}
+
+.header .spacer {
+  flex: 1;
+}
+
+.header .user-profile {
+  margin-left: 16px;
+}
+
+.header .avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  border: 2px solid rgba(255, 255, 255, 0.2);
+  object-fit: cover;
+}
+
+.icon-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border: none;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.08);
+  color: rgba(255, 255, 255, 0.92);
+  transition: background-color var(--transition-fast), transform var(--transition-fast);
+}
+
+.icon-btn:hover {
+  background: rgba(255, 255, 255, 0.18);
+  transform: translateY(-1px);
+}
+
+/* 主内容区域 */
+.main-content {
+  flex: 1;
+  height: calc(100vh - 52px);
+  display: flex;
+  min-height: 0;
+}
+
+/* 侧边栏 */
+.sidebar {
+  width: 268px;
+  background-color: rgba(255, 255, 255, 0.72);
+  border-right: 1px solid rgba(29, 29, 31, 0.06);
+  padding: 20px 0;
+  backdrop-filter: saturate(180%) blur(18px);
+}
+
+.nav-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.nav-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin: 4px 16px;
+  padding: 12px 16px;
+  border-radius: var(--pill-radius);
+  color: var(--text-secondary);
+  cursor: pointer;
+  transition: var(--transition-fast);
+  font-weight: 500;
+}
+
+.nav-item:hover {
+  background-color: rgba(29, 29, 31, 0.04);
+  color: var(--primary-color);
+}
+
+.nav-item.active {
+  background: rgba(0, 113, 227, 0.1);
+  color: var(--primary-color);
+  box-shadow: none;
+}
+
+.nav-item-divider {
+  height: 1px;
+  margin: 16px 20px 0;
+  background: var(--border-color);
+}
+
+.nav-text {
+  font-size: 15px;
+}
+
+.sidebar .mat-nav-list {
+  padding: 0;
+}
+
+.sidebar .mat-list-item {
+  height: 56px !important;
+  margin: 4px 16px;
+  border-radius: var(--pill-radius);
+  transition: var(--transition-fast);
+  color: var(--text-secondary);
+}
+
+.sidebar .mat-list-item:hover {
+  background-color: rgba(29, 29, 31, 0.04);
+  color: var(--primary-color);
+}
+
+.sidebar .mat-list-item.active {
+  background: rgba(0, 113, 227, 0.1);
+  color: var(--primary-color);
+  box-shadow: none;
+}
+
+.sidebar .mat-list-item .mat-icon {
+  margin-right: 16px;
+  color: inherit;
+}
+
+/* 工作区 */
+.workbench {
+  flex: 1;
+  min-width: 0;
+  padding: 28px;
+  overflow-y: auto;
+  background-color: var(--background-color);
+}
+
+/* 卡片容器 */
+.section-card {
+  background-color: var(--surface-elevated);
+  border-radius: var(--border-radius-lg);
+  padding: 36px;
+  box-shadow: var(--shadow-sm);
+  margin-bottom: 24px;
+  border: none;
+}
+
+/* 视频发现模块 */
+.search-header {
+  margin-bottom: 32px;
+}
+
+.search-header h2 {
+  font-size: 40px;
+  font-weight: 600;
+  line-height: 1.1;
+  letter-spacing: -0.02em;
+  color: var(--text-primary);
+  margin: 0 0 8px 0;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.search-header .subtitle {
+  color: var(--text-secondary);
+  font-size: 17px;
+  letter-spacing: -0.224px;
+  margin: 0;
+}
+
+.search-controls {
+  margin-bottom: 32px;
+}
+
+/* 博主监测模块 */
+.monitor-section {
+  background: var(--surface-elevated);
+}
+
+.monitor-header {
+  display: flex;
+  justify-content: space-between;
+  gap: 24px;
+  margin-bottom: 24px;
+  align-items: end;
+  flex-wrap: wrap;
+}
+
+.monitor-header h2 {
+  font-size: 40px;
+  font-weight: 600;
+  line-height: 1.1;
+  letter-spacing: -0.02em;
+  color: var(--text-primary);
+  margin: 0 0 8px 0;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.monitor-header .subtitle {
+  margin: 0;
+  color: var(--text-secondary);
+  font-size: 17px;
+  letter-spacing: -0.224px;
+}
+
+.monitor-toolbar {
+  display: flex;
+  gap: 16px;
+  align-items: end;
+  flex-wrap: wrap;
+  min-width: min(100%, 480px);
+}
+
+.monitor-toolbar .search-field {
+  min-width: 280px;
+}
+
+.monitor-error {
+  margin-bottom: 20px;
+  padding: 14px 16px;
+  border-radius: var(--border-radius-md);
+  background: rgba(245, 101, 101, 0.08);
+  border: 1px solid rgba(245, 101, 101, 0.2);
+  color: #c53030;
+}
+
+.monitor-summary {
+  display: flex;
+  gap: 16px;
+  margin-bottom: 24px;
+  flex-wrap: wrap;
+}
+
+.monitor-summary-card {
+  min-width: 160px;
+  padding: 18px 20px;
+  border-radius: var(--border-radius-lg);
+  background: var(--surface-elevated);
+  border: none;
+  box-shadow: var(--shadow-sm);
+}
+
+.monitor-summary-card strong {
+  display: block;
+  margin-top: 8px;
+  font-size: 28px;
+  color: var(--text-primary);
+}
+
+.monitor-summary-label {
+  color: var(--text-secondary);
+  font-size: 14px;
+}
+
+.monitor-author-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
+  gap: 20px;
+}
+
+.monitor-author-card {
+  padding: 22px;
+  border-radius: 12px;
+  background: var(--surface-elevated);
+  color: var(--text-primary);
+  box-shadow: var(--shadow-sm);
+  border: 1px solid var(--border-color);
+  cursor: pointer;
+  transition: transform var(--transition-normal), box-shadow var(--transition-normal);
+}
+
+.monitor-author-card:hover {
+  transform: translateY(-2px);
+  box-shadow: var(--shadow-md);
+}
+
+.monitor-author-top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.monitor-author-actions {
+  display: flex;
+  gap: 10px;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+}
+
+.monitor-author-identity {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  min-width: 0;
+}
+
+.monitor-author-avatar {
+  width: 68px;
+  height: 68px;
+  border-radius: 50%;
+  object-fit: cover;
+  flex-shrink: 0;
+  border: 2px solid rgba(29, 29, 31, 0.08);
+}
+
+.monitor-author-identity h3 {
+  margin: 0 0 6px 0;
+  font-size: 22px;
+  color: var(--text-primary);
+}
+
+.monitor-author-verified,
+.monitor-author-uid,
+.monitor-author-signature,
+.monitor-author-foot {
+  color: var(--text-secondary);
+}
+
+.monitor-author-verified,
+.monitor-author-uid {
+  margin: 0;
+  font-size: 13px;
+}
+
+.monitor-author-stats {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+  margin-bottom: 16px;
+  font-size: 14px;
+}
+
+.monitor-author-stat {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  min-width: 88px;
+  padding: 10px 12px;
+  border-radius: 12px;
+  background: rgba(29, 29, 31, 0.03);
+}
+
+.monitor-author-stat-label {
+  font-size: 12px;
+  color: var(--text-muted);
+}
+
+.monitor-author-stat-value {
+  color: var(--text-primary);
+  font-size: 20px;
+  line-height: 1.1;
+  font-weight: 600;
+}
+
+.monitor-author-signature {
+  margin: 0 0 16px 0;
+  line-height: 1.6;
+  font-size: 14px;
+  min-height: 44px;
+}
+
+.monitor-author-preview {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 10px;
+  margin-bottom: 12px;
+}
+
+.monitor-preview-item {
+  border-radius: 12px;
+  overflow: hidden;
+  aspect-ratio: 1 / 1;
+  background: rgba(29, 29, 31, 0.05);
+}
+
+.monitor-preview-item img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.monitor-author-foot {
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+  font-size: 13px;
+  padding-top: 12px;
+  border-top: 1px solid rgba(29, 29, 31, 0.06);
+}
+
+.monitor-detail-shell {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.monitor-detail-topbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+
+.monitor-detail-actions {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.monitor-hero {
+  border-radius: 24px;
+  min-height: 220px;
+  padding: 28px;
+  background-size: cover !important;
+  background-position: center !important;
+  color: white;
+  overflow: hidden;
+  box-shadow: 0 18px 42px rgba(15, 23, 42, 0.2);
+}
+
+.monitor-hero-main {
+  display: flex;
+  gap: 20px;
+  align-items: flex-start;
+}
+
+.monitor-hero-avatar {
+  width: 96px;
+  height: 96px;
+  border-radius: 50%;
+  object-fit: cover;
+  border: 3px solid rgba(255, 255, 255, 0.2);
+  flex-shrink: 0;
+}
+
+.monitor-hero-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.monitor-hero-title-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+  margin-bottom: 12px;
+}
+
+.monitor-hero-title-row h3 {
+  margin: 0;
+  font-size: 30px;
+}
+
+.monitor-hero-verified {
+  padding: 6px 10px;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.14);
+  font-size: 12px;
+}
+
+.monitor-hero-meta,
+.monitor-hero-submeta {
+  display: flex;
+  gap: 18px;
+  flex-wrap: wrap;
+  color: rgba(255, 255, 255, 0.86);
+  margin-bottom: 10px;
+}
+
+.monitor-hero-signature {
+  margin: 0;
+  color: rgba(255, 255, 255, 0.78);
+  line-height: 1.7;
+  max-width: 920px;
+}
+
+.monitor-work-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+
+.monitor-work-header h3 {
+  margin: 0 0 4px 0;
+  font-size: 24px;
+}
+
+.monitor-work-header p,
+.monitor-work-status {
+  margin: 0;
+  color: var(--text-secondary);
+}
+
+.monitor-works-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+  gap: 18px;
+}
+
+.monitor-work-card {
+  background: var(--surface-elevated);
+  border-radius: 12px;
+  overflow: hidden;
+  border: none;
+  box-shadow: var(--shadow-sm);
+}
+
+.monitor-work-cover {
+  position: relative;
+  aspect-ratio: 3 / 4;
+  background: #edf2f7;
+}
+
+.monitor-work-cover img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+}
+
+.monitor-work-badge,
+.monitor-work-duration {
+  position: absolute;
+  z-index: 1;
+  padding: 5px 9px;
+  border-radius: 999px;
+  font-size: 12px;
+  color: white;
+  background: rgba(15, 23, 42, 0.78);
+}
+
+.monitor-work-badge {
+  left: 12px;
+  top: 12px;
+}
+
+.monitor-work-duration {
+  right: 12px;
+  bottom: 12px;
+}
+
+.monitor-work-content {
+  padding: 14px;
+}
+
+.monitor-work-desc {
+  margin: 0 0 10px 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-primary);
+  line-height: 1.5;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.monitor-work-stats {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px 12px;
+  color: var(--text-secondary);
+  font-size: 12px;
+  margin-bottom: 8px;
+}
+
+.monitor-work-date {
+  font-size: 12px;
+  color: var(--text-muted);
+}
+
+.monitor-work-actions {
+  display: flex;
+  gap: 10px;
+  margin-top: 12px;
+}
+
+.monitor-work-actions .btn {
+  flex: 1;
+  justify-content: center;
+}
+
+.monitor-work-progress {
+  margin-top: 10px;
+}
+
+.monitor-work-progress-bar {
+  width: 100%;
+  height: 6px;
+  border-radius: 999px;
+  background: var(--border-color);
+  overflow: hidden;
+}
+
+.monitor-work-progress-fill {
+  height: 100%;
+  background: var(--primary-color);
+  transition: width 0.3s ease;
+}
+
+.monitor-load-more {
+  display: flex;
+  justify-content: center;
+}
+
+.search-input-group {
+  display: flex;
+  gap: 16px;
+  align-items: end;
+  flex-wrap: wrap;
+}
+
+.search-field {
+  flex: 1;
+  min-width: 200px;
+  overflow: hidden;
+}
+
+.filter-field {
+  width: 160px;
+}
+
+.search-btn {
+  height: 48px;
+  padding: 0 22px;
+  font-weight: 600;
+  font-size: 15px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.results-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+}
+
+.results-header h3 {
+  font-size: 21px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin: 0;
+}
+
+/* 视频网格 */
+.video-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+  gap: 24px;
+  margin-bottom: 24px;
+}
+
+.video-card {
+  position: relative;
+  height: 396px; /* 固定卡片整体高度,所有内部元素基于此做固定定位 */
+  border-radius: var(--border-radius-lg);
+  overflow: hidden;
+  transition: var(--transition-normal);
+  border: none;
+  background: var(--surface-elevated);
+  box-shadow: var(--shadow-sm);
+}
+
+.video-card:hover {
+  transform: translateY(-2px);
+  box-shadow: var(--shadow-md);
+}
+
+.video-thumbnail {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 200px;
+  min-height: 200px;
+  max-height: 200px;
+  overflow: hidden;
+}
+
+.video-thumbnail img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+  transition: var(--transition-normal);
+}
+
+.video-card:hover .video-thumbnail img {
+  transform: scale(1.05);
+}
+
+.video-duration {
+  position: absolute;
+  bottom: 8px;
+  right: 8px;
+  background: rgba(29, 29, 31, 0.88);
+  color: white;
+  padding: 4px 8px;
+  border-radius: var(--pill-radius);
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.video-meta-row {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+  align-items: center;
+  flex: 1;
+  min-width: 0;
+}
+
+.video-meta-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 10px;
+  border-radius: 999px;
+  font-size: 12px;
+  color: var(--text-secondary);
+  background: rgba(29, 29, 31, 0.04);
+}
+
+.video-info-row {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+}
+
+.video-author-inline {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.video-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin: 16px 0 10px 0;
+  line-height: 1.4;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.author-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12px;
+}
+
+.author-name {
+  color: var(--text-secondary);
+  font-weight: 500;
+}
+
+.video-id {
+  font-size: 12px;
+  color: var(--text-muted);
+}
+
+.video-interactions {
+  display: inline-flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.interaction-stat {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--text-secondary);
+  white-space: nowrap;
+}
+
+.interaction-stat::before {
+  content: '';
+  display: inline-block;
+  width: 14px;
+  height: 14px;
+  background-color: currentColor;
+  -webkit-mask-repeat: no-repeat;
+  -webkit-mask-position: center;
+  -webkit-mask-size: contain;
+  mask-repeat: no-repeat;
+  mask-position: center;
+  mask-size: contain;
+}
+
+.interaction-stat.like::before {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54z'/%3E%3C/svg%3E");
+}
+
+.interaction-stat.comment::before {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M20 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z'/%3E%3C/svg%3E");
+}
+
+.interaction-stat.share::before {
+  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.5 2.5 0 0 0 0-1.39l7-4.11A2.99 2.99 0 1 0 15 5a2.9 2.9 0 0 0 .04.49l-7.13 4.2a3 3 0 1 0 0 4.62l7.12 4.19c-.03.15-.03.3-.03.46a3 3 0 1 0 3-2.88z'/%3E%3C/svg%3E");
+  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='black' d='M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.5 2.5 0 0 0 0-1.39l7-4.11A2.99 2.99 0 1 0 15 5a2.9 2.9 0 0 0 .04.49l-7.13 4.2a3 3 0 1 0 0 4.62l7.12 4.19c-.03.15-.03.3-.03.46a3 3 0 1 0 3-2.88z'/%3E%3C/svg%3E");
+}
+
+.video-stats {
+  display: flex;
+  gap: 12px;
+}
+
+.video-stat-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.video-stat-grid .stat-item {
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  padding: 10px 12px;
+  border-radius: 12px;
+  background: rgba(29, 29, 31, 0.03);
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: var(--text-muted);
+  font-size: 14px;
+}
+
+.stat-item-primary {
+  color: var(--primary-color);
+  font-weight: 600;
+}
+
+.stat-label-inline {
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.tag-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.tag-list .tag,
+.video-tags .tag {
+  padding: 4px 10px;
+  border-radius: var(--pill-radius);
+  background: rgba(0, 113, 227, 0.08);
+  color: var(--primary-dark);
+  font-size: 12px;
+}
+
+.video-tags {
+  margin-bottom: 16px;
+}
+
+.video-tags .mat-chip {
+  font-size: 12px;
+  height: 24px;
+  margin: 2px;
+}
+
+/* 任务管理模块 */
+.tasks-header {
+  margin-bottom: 32px;
+}
+
+.tasks-header h2 {
+  font-size: 40px;
+  font-weight: 600;
+  line-height: 1.1;
+  letter-spacing: -0.02em;
+  color: var(--text-primary);
+  margin: 0 0 8px 0;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.tasks-overview {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 20px;
+  margin-bottom: 32px;
+}
+
+.overview-card {
+  padding: 20px;
+  text-align: center;
+  border-radius: var(--border-radius-md);
+  border: 1px solid var(--border-color);
+}
+
+.overview-item {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12px;
+}
+
+.overview-item .mat-icon {
+  font-size: 32px;
+  width: 32px;
+  height: 32px;
+}
+
+.overview-content {
+  display: flex;
+  flex-direction: column;
+  align-items: start;
+}
+
+.overview-number {
+  font-size: 24px;
+  font-weight: 700;
+  color: var(--text-primary);
+}
+
+.overview-label {
+  font-size: 14px;
+  color: var(--text-secondary);
+}
+
+.active-tasks h3 {
+  font-size: 20px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin-bottom: 20px;
+}
+
+.task-card {
+  margin-bottom: 20px;
+  padding: 24px;
+  border-radius: var(--border-radius-md);
+  border: none;
+  background: var(--surface-elevated);
+  box-shadow: var(--shadow-sm);
+}
+
+.task-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.task-info {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.task-id {
+  font-family: 'Monaco', 'Menlo', monospace;
+  font-size: 12px;
+  color: var(--text-muted);
+}
+
+.task-keyword {
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.task-progress {
+  margin-bottom: 24px;
+}
+
+.progress-info {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 14px;
+  color: var(--text-secondary);
+}
+
+.workflow-steps {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 20px;
+  position: relative;
+}
+
+.workflow-steps::before {
+  content: '';
+  position: absolute;
+  top: 20px;
+  left: 40px;
+  right: 40px;
+  height: 2px;
+  background: var(--border-color);
+  z-index: 1;
+}
+
+.step {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  position: relative;
+  z-index: 2;
+}
+
+.step-icon {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background: var(--border-color);
+  display: none;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 8px;
+  transition: var(--transition-normal);
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.step-icon .mat-icon {
+  color: var(--text-muted);
+  font-size: 20px;
+}
+
+.step-label {
+  font-size: 12px;
+  color: var(--text-muted);
+  text-align: center;
+  white-space: nowrap;
+}
+
+.step.completed .step-icon {
+  background: var(--success-color);
+}
+
+.step.completed .step-icon .mat-icon {
+  color: white;
+}
+
+.step.completed .step-label {
+  color: var(--success-color);
+  font-weight: 600;
+}
+
+.step.active .step-icon {
+  background: var(--primary-color);
+  box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.16);
+}
+
+.step.active .step-icon .mat-icon {
+  color: white;
+}
+
+.step.active .step-label {
+  color: var(--primary-color);
+  font-weight: 600;
+}
+
+.task-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+
+/* ==================== 生成历史 ==================== */
+/* 工具栏:状态筛选 + 导出 */
+.history-page .history-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin: 12px 0 20px;
+  flex-wrap: wrap;
+}
+
+.history-toolbar__filter {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 12px;
+  backdrop-filter: blur(10px);
+}
+
+.history-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 7px 14px;
+  border: 0;
+  border-radius: 9px;
+  background: transparent;
+  color: var(--dh-text-2);
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  white-space: nowrap;
+}
+.history-pill:hover { background: var(--dh-border); color: var(--dh-text); }
+.history-pill--active {
+  background: linear-gradient(135deg, #6366f1, #8b5cf6);
+  color: #fff;
+  box-shadow: 0 4px 14px rgba(99, 102, 241, 0.32);
+}
+.history-pill--success.history-pill--active {
+  background: linear-gradient(135deg, #10b981, #34d399);
+  box-shadow: 0 4px 14px rgba(16, 185, 129, 0.32);
+}
+.history-pill--danger.history-pill--active {
+  background: linear-gradient(135deg, #ef4444, #f87171);
+  box-shadow: 0 4px 14px rgba(239, 68, 68, 0.32);
+}
+.history-pill__count {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 22px;
+  padding: 0 7px;
+  height: 18px;
+  font-size: 11px;
+  font-weight: 600;
+  border-radius: 9px;
+  background: rgba(0, 0, 0, 0.18);
+  color: inherit;
+}
+.history-pill--active .history-pill__count {
+  background: rgba(255, 255, 255, 0.25);
+  color: #fff;
+}
+
+.history-export {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  padding: 9px 16px;
+  border-radius: 10px;
+  border: 1px solid var(--dh-border);
+  background: var(--dh-card);
+  color: var(--dh-text);
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.18s ease;
+}
+.history-export:hover {
+  border-color: var(--dh-border-2);
+  background: var(--dh-card-2);
+  transform: translateY(-1px);
+}
+
+/* 列表 */
+.history-page .history-list {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+/* 卡片 */
+.history-page .history-card {
+  position: relative;
+  display: grid;
+  grid-template-columns: 1fr auto auto;
+  align-items: center;
+  gap: 24px;
+  padding: 18px 22px 18px 26px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 14px;
+  backdrop-filter: blur(10px);
+  transition: all 0.2s ease;
+  overflow: hidden;
+}
+.history-page .history-card:hover {
+  border-color: var(--dh-border-2);
+  transform: translateY(-1px);
+  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
+}
+
+.history-card__accent {
+  position: absolute;
+  left: 0; top: 0; bottom: 0;
+  width: 3px;
+  background: linear-gradient(180deg, #6366f1, #8b5cf6);
+}
+.history-card--success .history-card__accent {
+  background: linear-gradient(180deg, #10b981, #34d399);
+}
+.history-card--failed .history-card__accent {
+  background: linear-gradient(180deg, #ef4444, #f87171);
+}
+
+.history-card__body {
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.history-card__head {
+  display: flex;
+  align-items: baseline;
+  justify-content: space-between;
+  gap: 16px;
+}
+.history-card__title {
+  margin: 0;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--dh-text);
+  line-height: 1.4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+.history-card__date {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  flex-shrink: 0;
+  font-size: 12px;
+  color: var(--dh-text-3);
+  font-variant-numeric: tabular-nums;
+}
+.history-card__date svg { opacity: 0.7; }
+
+.history-card__tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+.history-tag {
+  display: inline-flex;
+  align-items: center;
+  padding: 3px 10px;
+  font-size: 11px;
+  font-weight: 500;
+  color: #6366f1;
+  background: rgba(99, 102, 241, 0.1);
+  border-radius: 999px;
+}
+
+.history-card__meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+}
+
+.history-status-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 10px 4px 8px;
+  border-radius: 999px;
+  font-size: 11px;
+  font-weight: 600;
+  background: rgba(99, 102, 241, 0.12);
+  color: #6366f1;
+}
+.history-status-pill__dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: currentColor;
+  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
+}
+.history-status-pill--success {
+  background: rgba(16, 185, 129, 0.12);
+  color: #10b981;
+}
+.history-status-pill--success .history-status-pill__dot {
+  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.18);
+}
+.history-status-pill--failed {
+  background: rgba(239, 68, 68, 0.12);
+  color: #ef4444;
+}
+.history-status-pill--failed .history-status-pill__dot {
+  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18);
+}
+
+.history-chip {
+  display: inline-flex;
+  align-items: center;
+  padding: 3px 10px;
+  font-size: 11px;
+  font-weight: 500;
+  color: var(--dh-text-2);
+  background: var(--dh-border);
+  border-radius: 999px;
+}
+
+/* 费用块 */
+.history-card__cost {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: 2px;
+  padding: 0 4px;
+  min-width: 72px;
+}
+.history-card__cost-label {
+  font-size: 11px;
+  color: var(--dh-text-3);
+  letter-spacing: 0.02em;
+}
+.history-card__cost-value {
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--dh-text);
+  font-variant-numeric: tabular-nums;
+  line-height: 1.1;
+}
+
+/* 操作按钮 */
+.history-card__actions {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+}
+.history-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 8px 14px;
+  border: 0;
+  border-radius: 9px;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  white-space: nowrap;
+}
+.history-btn--primary {
+  background: linear-gradient(135deg, #3b82f6, #6366f1);
+  color: #fff;
+  box-shadow: 0 4px 14px rgba(59, 130, 246, 0.28);
+}
+.history-btn--primary:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 6px 18px rgba(59, 130, 246, 0.4);
+}
+.history-btn--ghost {
+  background: transparent;
+  color: var(--dh-text-2);
+  border: 1px solid var(--dh-border);
+  padding: 8px 10px;
+}
+.history-btn--ghost:hover {
+  color: #ef4444;
+  border-color: rgba(239, 68, 68, 0.4);
+  background: rgba(239, 68, 68, 0.06);
+}
+
+/* 空状态 */
+.history-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  padding: 64px 24px;
+  background: var(--dh-card);
+  border: 1px dashed var(--dh-border-2);
+  border-radius: 14px;
+  gap: 8px;
+}
+.history-empty__icon {
+  width: 64px;
+  height: 64px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  background: var(--dh-border);
+  color: var(--dh-text-2);
+  margin-bottom: 4px;
+}
+.history-empty h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--dh-text);
+}
+.history-empty p {
+  margin: 0;
+  font-size: 13px;
+  color: var(--dh-text-3);
+}
+.history-empty .history-btn {
+  margin-top: 12px;
+}
+
+/* 响应式 */
+@media (max-width: 880px) {
+  .history-page .history-card {
+    grid-template-columns: 1fr;
+    gap: 14px;
+  }
+  .history-card__cost { flex-direction: row; align-items: center; gap: 8px; min-width: 0; }
+  .history-card__actions { justify-content: flex-end; }
+  .history-card__head { flex-wrap: wrap; }
+}
+@media (max-width: 540px) {
+  .history-page .history-toolbar { gap: 10px; }
+  .history-toolbar__filter { width: 100%; justify-content: space-between; }
+  .history-pill { flex: 1; justify-content: center; }
+  .history-export { width: 100%; justify-content: center; }
+}
+
+/* ==================== 任务管理(重设计) ==================== */
+/* 顶部统计卡 */
+.tasks-page .task-stats {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+  gap: 14px;
+  margin: 12px 0 24px;
+}
+.tasks-page .task-stat {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 16px 18px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 14px;
+  text-align: left;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  backdrop-filter: blur(10px);
+  overflow: hidden;
+}
+.tasks-page .task-stat::before {
+  content: '';
+  position: absolute;
+  inset: 0;
+  background: var(--task-stat-glow, transparent);
+  opacity: 0;
+  transition: opacity 0.2s ease;
+  pointer-events: none;
+}
+.tasks-page .task-stat:hover {
+  transform: translateY(-2px);
+  border-color: var(--dh-border-2);
+  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18);
+}
+.tasks-page .task-stat:hover::before { opacity: 1; }
+.tasks-page .task-stat--active {
+  border-color: transparent;
+  box-shadow: 0 8px 24px var(--task-stat-shadow, rgba(99, 102, 241, 0.25));
+}
+.tasks-page .task-stat--active::before { opacity: 1; }
+
+.task-stat__icon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 42px;
+  height: 42px;
+  border-radius: 12px;
+  background: var(--task-stat-icon-bg, rgba(99, 102, 241, 0.14));
+  color: var(--task-stat-color, #6366f1);
+  flex-shrink: 0;
+  z-index: 1;
+}
+.task-stat__body {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  z-index: 1;
+}
+.task-stat__number {
+  font-size: 24px;
+  font-weight: 700;
+  color: var(--dh-text);
+  line-height: 1;
+  font-variant-numeric: tabular-nums;
+}
+.task-stat__label {
+  font-size: 12px;
+  color: var(--dh-text-3);
+  letter-spacing: 0.02em;
+}
+.tasks-page .task-stat--active .task-stat__number { color: #fff; }
+.tasks-page .task-stat--active .task-stat__label { color: rgba(255, 255, 255, 0.85); }
+.tasks-page .task-stat--active .task-stat__icon {
+  background: rgba(255, 255, 255, 0.22);
+  color: #fff;
+}
+
+/* 各状态色板 */
+.tasks-page .task-stat--pending {
+  --task-stat-color: #f59e0b;
+  --task-stat-icon-bg: rgba(245, 158, 11, 0.14);
+  --task-stat-glow: linear-gradient(135deg, rgba(245, 158, 11, 0.16), rgba(251, 191, 36, 0.06));
+  --task-stat-shadow: rgba(245, 158, 11, 0.32);
+}
+.tasks-page .task-stat--pending.task-stat--active {
+  background: linear-gradient(135deg, #f59e0b, #fbbf24);
+}
+.tasks-page .task-stat--processing {
+  --task-stat-color: #3b82f6;
+  --task-stat-icon-bg: rgba(59, 130, 246, 0.14);
+  --task-stat-glow: linear-gradient(135deg, rgba(59, 130, 246, 0.16), rgba(99, 102, 241, 0.06));
+  --task-stat-shadow: rgba(59, 130, 246, 0.32);
+}
+.tasks-page .task-stat--processing.task-stat--active {
+  background: linear-gradient(135deg, #3b82f6, #6366f1);
+}
+.tasks-page .task-stat--processing .task-stat__icon svg { animation: task-spin 2s linear infinite; }
+.tasks-page .task-stat--completed {
+  --task-stat-color: #10b981;
+  --task-stat-icon-bg: rgba(16, 185, 129, 0.14);
+  --task-stat-glow: linear-gradient(135deg, rgba(16, 185, 129, 0.16), rgba(52, 211, 153, 0.06));
+  --task-stat-shadow: rgba(16, 185, 129, 0.32);
+}
+.tasks-page .task-stat--completed.task-stat--active {
+  background: linear-gradient(135deg, #10b981, #34d399);
+}
+.tasks-page .task-stat--failed {
+  --task-stat-color: #ef4444;
+  --task-stat-icon-bg: rgba(239, 68, 68, 0.14);
+  --task-stat-glow: linear-gradient(135deg, rgba(239, 68, 68, 0.16), rgba(248, 113, 113, 0.06));
+  --task-stat-shadow: rgba(239, 68, 68, 0.32);
+}
+.tasks-page .task-stat--failed.task-stat--active {
+  background: linear-gradient(135deg, #ef4444, #f87171);
+}
+@keyframes task-spin { to { transform: rotate(360deg); } }
+
+/* 列表区头部 */
+.task-list-section__head {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 14px;
+  gap: 12px;
+}
+.task-list-section__title {
+  display: inline-flex;
+  align-items: center;
+  gap: 10px;
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--dh-text);
+}
+.task-list-section__count {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 26px;
+  padding: 0 8px;
+  height: 22px;
+  font-size: 12px;
+  font-weight: 600;
+  border-radius: 11px;
+  background: var(--dh-border);
+  color: var(--dh-text-2);
+  font-variant-numeric: tabular-nums;
+}
+
+/* 任务列表 */
+.tasks-page .task-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+/* 单条任务卡 */
+.tasks-page .task-row {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  padding: 16px 20px 16px 24px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 14px;
+  backdrop-filter: blur(10px);
+  transition: all 0.2s ease;
+  overflow: hidden;
+}
+.tasks-page .task-row:hover {
+  border-color: var(--dh-border-2);
+  transform: translateY(-1px);
+  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
+}
+.task-row__accent {
+  position: absolute;
+  left: 0; top: 0; bottom: 0;
+  width: 3px;
+  background: linear-gradient(180deg, #f59e0b, #fbbf24);
+}
+.task-row--processing .task-row__accent { background: linear-gradient(180deg, #3b82f6, #6366f1); }
+.task-row--success .task-row__accent { background: linear-gradient(180deg, #10b981, #34d399); }
+.task-row--failed .task-row__accent { background: linear-gradient(180deg, #ef4444, #f87171); }
+
+/* 头部行 */
+.task-row__head {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+.task-row__title-wrap {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  min-width: 0;
+  flex: 1;
+}
+.task-row__type-badge {
+  display: inline-flex;
+  align-items: center;
+  padding: 3px 8px;
+  border-radius: 6px;
+  background: linear-gradient(135deg, rgba(99, 102, 241, 0.16), rgba(139, 92, 246, 0.16));
+  color: #818cf8;
+  font-size: 11px;
+  font-weight: 600;
+  letter-spacing: 0.02em;
+  flex-shrink: 0;
+}
+.task-row__title {
+  margin: 0;
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--dh-text);
+  line-height: 1.4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  min-width: 0;
+}
+.task-row__head-right {
+  display: inline-flex;
+  align-items: center;
+  gap: 12px;
+  flex-shrink: 0;
+}
+
+.task-row__status-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 10px 4px 8px;
+  border-radius: 999px;
+  font-size: 11px;
+  font-weight: 600;
+  background: rgba(99, 102, 241, 0.12);
+  color: #6366f1;
+}
+.task-row__status-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: currentColor;
+  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.18);
+}
+.task-row__status-pill--pending {
+  background: rgba(245, 158, 11, 0.14);
+  color: #f59e0b;
+}
+.task-row__status-pill--pending .task-row__status-dot { box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.18); }
+.task-row__status-pill--processing {
+  background: rgba(59, 130, 246, 0.14);
+  color: #3b82f6;
+}
+.task-row__status-pill--processing .task-row__status-dot {
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.18);
+  animation: task-pulse 1.4s ease-in-out infinite;
+}
+@keyframes task-pulse {
+  0%, 100% { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.18); }
+  50% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0.05); }
+}
+.task-row__status-pill--success {
+  background: rgba(16, 185, 129, 0.14);
+  color: #10b981;
+}
+.task-row__status-pill--success .task-row__status-dot { box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.18); }
+.task-row__status-pill--failed {
+  background: rgba(239, 68, 68, 0.14);
+  color: #ef4444;
+}
+.task-row__status-pill--failed .task-row__status-dot { box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.18); }
+
+.task-row__date {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 12px;
+  color: var(--dh-text-3);
+  font-variant-numeric: tabular-nums;
+}
+.task-row__date svg { opacity: 0.7; }
+
+/* 进度区 */
+.task-row__progress { display: flex; flex-direction: column; gap: 6px; }
+.task-row__progress-info {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 12px;
+}
+.task-row__stage { color: var(--dh-text-2); }
+.task-row__progress-pct {
+  font-weight: 600;
+  color: var(--dh-text);
+  font-variant-numeric: tabular-nums;
+}
+.task-row__progress-bar {
+  height: 6px;
+  border-radius: 999px;
+  background: var(--dh-border);
+  overflow: hidden;
+}
+.task-row__progress-fill {
+  height: 100%;
+  border-radius: 999px;
+  background: linear-gradient(90deg, #3b82f6, #6366f1);
+  transition: width 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
+  position: relative;
+}
+.task-row--processing .task-row__progress-fill::after {
+  content: '';
+  position: absolute;
+  inset: 0;
+  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent);
+  animation: task-shimmer 1.6s linear infinite;
+}
+@keyframes task-shimmer {
+  from { transform: translateX(-100%); }
+  to { transform: translateX(100%); }
+}
+.task-row__progress-fill--success { background: linear-gradient(90deg, #10b981, #34d399); }
+.task-row__progress-fill--failed { background: linear-gradient(90deg, #ef4444, #f87171); }
+
+/* 元信息 chips */
+.task-row__meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.task-row__chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  padding: 4px 10px;
+  font-size: 11px;
+  color: var(--dh-text-2);
+  background: var(--dh-border);
+  border-radius: 999px;
+}
+.task-row__chip svg { opacity: 0.75; }
+
+/* 错误条 */
+.task-row__error {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  background: rgba(239, 68, 68, 0.08);
+  border: 1px solid rgba(239, 68, 68, 0.2);
+  color: #ef4444;
+  font-size: 12px;
+  line-height: 1.5;
+}
+.task-row__error svg { flex-shrink: 0; margin-top: 1px; }
+
+/* 操作按钮 */
+.task-row__actions {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+.task-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 7px 14px;
+  border: 0;
+  border-radius: 9px;
+  font-size: 12px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  white-space: nowrap;
+  text-decoration: none;
+}
+.task-btn--primary {
+  background: linear-gradient(135deg, #3b82f6, #6366f1);
+  color: #fff;
+  box-shadow: 0 4px 14px rgba(59, 130, 246, 0.28);
+}
+.task-btn--primary:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(59, 130, 246, 0.4); }
+.task-btn--success {
+  background: linear-gradient(135deg, #10b981, #34d399);
+  color: #fff;
+  box-shadow: 0 4px 14px rgba(16, 185, 129, 0.28);
+}
+.task-btn--success:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(16, 185, 129, 0.4); }
+.task-btn--ghost {
+  background: transparent;
+  color: var(--dh-text-2);
+  border: 1px solid var(--dh-border);
+}
+.task-btn--ghost:hover {
+  color: var(--dh-text);
+  border-color: var(--dh-border-2);
+  background: var(--dh-border);
+}
+.task-btn--danger {
+  background: transparent;
+  color: var(--dh-text-2);
+  border: 1px solid var(--dh-border);
+  padding: 7px 9px;
+}
+.task-btn--danger:hover {
+  color: #ef4444;
+  border-color: rgba(239, 68, 68, 0.4);
+  background: rgba(239, 68, 68, 0.06);
+}
+
+/* 空状态(独立 + 内联) */
+.task-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  padding: 56px 24px;
+  background: var(--dh-card);
+  border: 1px dashed var(--dh-border-2);
+  border-radius: 14px;
+  gap: 8px;
+}
+.task-empty__icon {
+  width: 64px;
+  height: 64px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  background: var(--dh-border);
+  color: var(--dh-text-2);
+  margin-bottom: 4px;
+}
+.task-empty h3 { margin: 0; font-size: 16px; font-weight: 600; color: var(--dh-text); }
+.task-empty p { margin: 0; font-size: 13px; color: var(--dh-text-3); }
+.task-empty--inline { padding: 28px 20px; gap: 10px; }
+.task-empty--inline p { color: var(--dh-text-2); font-size: 13px; }
+
+/* 屏蔽冲突的旧样式 */
+.tasks-page .tasks-overview,
+.tasks-page .overview-card,
+.tasks-page .task-card,
+.tasks-page .active-tasks h3 {
+  /* 旧版结构已不再渲染,这些选择器留空避免被其他全局规则误命中 */
+  all: unset;
+}
+
+/* 响应式 */
+@media (max-width: 720px) {
+  .task-row__head { flex-direction: column; align-items: flex-start; gap: 10px; }
+  .task-row__head-right { width: 100%; justify-content: space-between; }
+}
+@media (max-width: 540px) {
+  .tasks-page .task-stats { grid-template-columns: repeat(2, 1fr); }
+  .task-stat__number { font-size: 20px; }
+}
+
+/* 结果展示模块 */
+.results-header {
+  margin-bottom: 32px;
+}
+
+.results-header h2 {
+  font-size: 40px;
+  font-weight: 600;
+  line-height: 1.1;
+  letter-spacing: -0.02em;
+  color: var(--text-primary);
+  margin: 0 0 8px 0;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.results-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  gap: 24px;
+}
+
+.result-card {
+  border-radius: var(--border-radius-lg);
+  overflow: hidden;
+  border: none;
+  background-color: var(--surface-elevated);
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  transition: var(--transition-normal);
+  box-shadow: var(--shadow-sm);
+}
+
+.result-card:hover {
+  transform: translateY(-2px);
+  box-shadow: var(--shadow-md);
+}
+
+.result-media {
+  position: relative;
+  height: 200px;
+  overflow: hidden;
+}
+
+.result-media video,
+.result-media img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.result-badge {
+  position: absolute;
+  top: 12px;
+  left: 12px;
+  background: rgba(0, 0, 0, 0.8);
+  color: white;
+  padding: 6px 12px;
+  border-radius: var(--border-radius-sm);
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.result-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin: 16px 0 12px 0;
+}
+
+.result-meta {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.result-date {
+  color: var(--text-muted);
+  font-size: 14px;
+}
+
+.result-quality {
+  background: rgba(29, 29, 31, 0.08);
+  color: var(--text-primary);
+  padding: 2px 8px;
+  border-radius: var(--border-radius-sm);
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.result-stats {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  margin-bottom: 16px;
+  padding-top: 12px;
+  border-top: 1px solid var(--border-color);
+}
+
+.result-content {
+  padding: 16px;
+  flex: 1;
+}
+
+.result-actions {
+  padding: 16px;
+  border-top: 1px solid var(--border-color);
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+  background-color: rgba(29, 29, 31, 0.02);
+}
+
+.result-stats .stat-item {
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.voice-layout {
+  display: grid;
+  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
+  gap: 20px;
+  align-items: start;
+}
+
+.voice-create-panel,
+.voice-list-panel {
+  background: var(--surface-elevated);
+  border-radius: 16px;
+  border: 1px solid rgba(29, 29, 31, 0.06);
+  box-shadow: var(--shadow-sm);
+  padding: 22px;
+}
+
+.voice-panel-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 18px;
+}
+
+.voice-panel-head h3 {
+  margin: 0;
+  font-size: 20px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.voice-panel-meta {
+  font-size: 13px;
+  color: var(--text-secondary);
+}
+
+.voice-mode-switch {
+  display: flex;
+  gap: 10px;
+  flex-wrap: wrap;
+  margin-bottom: 12px;
+}
+
+.voice-mode-hint {
+  margin-bottom: 16px;
+  padding: 12px 14px;
+  border-radius: 12px;
+  background: rgba(0, 113, 227, 0.08);
+  color: var(--text-secondary);
+  font-size: 13px;
+  line-height: 1.6;
+}
+
+.voice-form-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+}
+
+.voice-form-span-2,
+.voice-form-image-field {
+  grid-column: span 2;
+}
+
+.voice-image-picker {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+
+.voice-image-name {
+  font-size: 13px;
+  color: var(--text-secondary);
+  word-break: break-all;
+}
+
+.voice-create-actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 18px;
+}
+
+.voice-list {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.voice-card {
+  border: 1px solid rgba(29, 29, 31, 0.06);
+  border-radius: 14px;
+  padding: 16px;
+  background: rgba(251, 251, 253, 0.9);
+}
+
+.voice-card-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.voice-card-top h4 {
+  margin: 0 0 4px 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.voice-card-top p {
+  margin: 0;
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.voice-meta-grid {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.voice-meta-chip {
+  display: inline-flex;
+  align-items: center;
+  padding: 4px 10px;
+  border-radius: var(--pill-radius);
+  font-size: 12px;
+  color: var(--text-secondary);
+  background: rgba(29, 29, 31, 0.04);
+}
+
+.voice-text-block {
+  padding: 10px 12px;
+  border-radius: 12px;
+  background: rgba(29, 29, 31, 0.03);
+  color: var(--text-secondary);
+  font-size: 13px;
+  line-height: 1.6;
+  margin-bottom: 10px;
+}
+
+.voice-text-block.sample {
+  color: var(--text-primary);
+}
+
+.voice-audio-player {
+  width: 100%;
+  margin-top: 4px;
+  margin-bottom: 12px;
+}
+
+.voice-audio-player.compact {
+  margin: 0;
+  max-width: 280px;
+}
+
+.voice-audio-caption {
+  font-size: 12px;
+  color: var(--text-secondary);
+  margin-bottom: 8px;
+}
+
+.voice-synthesis-box {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin-bottom: 12px;
+  padding: 12px;
+  border-radius: 12px;
+  background: rgba(29, 29, 31, 0.03);
+}
+
+.voice-synthesis-box .form-field {
+  margin: 0;
+}
+
+.voice-card-actions {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+/* 空状态 */
+.empty-state,
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 60px 20px;
+  text-align: center;
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.52);
+}
+
+.empty-state .mat-icon,
+.loading-state .mat-icon {
+  font-size: 64px;
+  width: 64px;
+  height: 64px;
+  color: var(--text-muted);
+  margin-bottom: 20px;
+}
+
+.empty-state .icon.large,
+.loading-state .icon.large {
+  font-size: 56px;
+  color: var(--text-muted);
+  margin-bottom: 16px;
+}
+
+.empty-state h3,
+.loading-state h3 {
+  font-size: 20px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin: 0 0 8px 0;
+}
+
+.empty-state p,
+.loading-state p {
+  color: var(--text-secondary);
+  margin: 0;
+  max-width: 400px;
+}
+
+.loading-state {
+  gap: 16px;
+}
+
+/* 响应式设计 */
+@media (max-width: 1200px) {
+  .video-grid {
+    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  }
+
+  .results-grid {
+    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  }
+
+  .monitor-works-grid {
+    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+  }
+
+  .voice-layout,
+  .voice-form-grid,
+  .remix-audio-source-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .voice-form-span-2,
+  .voice-form-image-field {
+    grid-column: span 1;
+  }
+
+  .voice-selected-card {
+    align-items: flex-start;
+  }
+}
+
+@media (max-width: 768px) {
+  .sidebar {
+    width: 240px;
+  }
+
+  .workbench {
+    padding: 16px;
+  }
+
+  .section-card {
+    padding: 20px;
+  }
+
+  .search-input-group {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .search-field,
+  .filter-field {
+    min-width: auto;
+    width: 100%;
+  }
+
+  .video-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .results-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .monitor-author-grid,
+  .monitor-works-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .monitor-header,
+  .monitor-toolbar,
+  .monitor-author-top,
+  .monitor-detail-topbar,
+  .monitor-hero-main,
+  .monitor-author-foot {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .monitor-toolbar .search-field {
+    min-width: 100%;
+  }
+
+  .tasks-overview {
+    grid-template-columns: repeat(2, 1fr);
+  }
+
+  .workflow-steps {
+    flex-direction: column;
+    gap: 16px;
+  }
+
+  .workflow-steps::before {
+    display: none;
+  }
+
+  .history-item {
+    flex-direction: column;
+    align-items: stretch;
+    gap: 16px;
+  }
+
+  .history-actions {
+    justify-content: center;
+  }
+}
+
+@media (max-width: 480px) {
+  .header {
+    padding: 0 16px;
+  }
+
+  .header .logo {
+    font-size: 16px;
+  }
+
+  .main-content {
+    flex-direction: column;
+  }
+
+  .sidebar {
+    width: 100%;
+    height: auto;
+    border-right: none;
+    border-bottom: 1px solid var(--border-color);
+  }
+
+  .tasks-overview {
+    grid-template-columns: 1fr;
+  }
+}
+
+/* 动画效果 */
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.5;
+  }
+}
+
+.section-card {
+  animation: fadeIn 0.5s ease-out;
+}
+
+.loading-state .mat-spinner {
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+/* 自定义滚动条 */
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+::-webkit-scrollbar-track {
+  background: var(--background-color);
+}
+
+::-webkit-scrollbar-thumb {
+  background: var(--border-color);
+  border-radius: var(--border-radius-sm);
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: var(--text-muted);
+}
+
+/* 视频管理模块 */
+.videos-header {
+  margin-bottom: 32px;
+}
+
+.videos-header h2 {
+  font-size: 40px;
+  font-weight: 600;
+  line-height: 1.1;
+  letter-spacing: -0.02em;
+  color: var(--text-primary);
+  margin: 0 0 8px 0;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.videos-controls {
+  display: flex;
+  gap: 16px;
+  align-items: end;
+  flex-wrap: wrap;
+}
+
+.videos-actions {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 24px;
+  background-color: var(--background-color);
+  border-radius: var(--border-radius-md);
+  margin-bottom: 24px;
+  border: 1px solid var(--border-color);
+}
+
+.selection-info {
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.batch-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.videos-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+  gap: 24px;
+  margin-bottom: 24px;
+}
+
+.video-item {
+  position: relative;
+  border-radius: var(--border-radius-lg);
+  overflow: hidden;
+  border: none;
+  background-color: var(--surface-elevated);
+  transition: var(--transition-normal);
+  display: flex;
+  flex-direction: column;
+  box-shadow: var(--shadow-sm);
+}
+
+.video-item:hover {
+  transform: translateY(-2px);
+  box-shadow: var(--shadow-md);
+}
+
+.video-item.selected {
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.18), var(--shadow-sm);
+}
+
+.video-checkbox {
+  position: absolute;
+  top: 12px;
+  left: 12px;
+  z-index: 10;
+}
+
+.video-checkbox input[type="checkbox"] {
+  width: 20px;
+  height: 20px;
+  cursor: pointer;
+}
+
+.video-thumbnail {
+  position: relative;
+  height: 180px;
+  overflow: hidden;
+}
+
+.video-thumbnail img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  transition: var(--transition-normal);
+}
+
+.video-item:hover .video-thumbnail img,
+.video-item:hover .video-thumbnail video {
+  transform: scale(1.05);
+}
+
+.video-thumbnail video {
+  transition: var(--transition-normal);
+}
+
+.video-thumb-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: var(--bg-tertiary);
+}
+
+.video-source {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  padding: 4px 8px;
+  border-radius: var(--border-radius-sm);
+  font-size: 11px;
+  font-weight: 600;
+  color: white;
+  text-transform: uppercase;
+}
+
+.video-source.downloaded {
+  background-color: var(--success-color);
+}
+
+.video-source.generated {
+  background-color: var(--primary-color);
+}
+
+.video-source.uploaded {
+  background-color: var(--surface-dark);
+}
+
+.video-info {
+  padding: 16px;
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.video-title-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.video-title-row h4 {
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--text-primary);
+  margin: 0;
+  line-height: 1.4;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  flex: 1;
+  margin-right: 12px;
+}
+
+.category-badge {
+  padding: 4px 8px;
+  border-radius: var(--border-radius-sm);
+  font-size: 11px;
+  font-weight: 600;
+  color: white;
+  white-space: nowrap;
+}
+
+.video-meta {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.video-meta .meta-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: var(--text-muted);
+  font-size: 13px;
+}
+
+.video-meta .meta-item .icon {
+  font-size: 16px;
+}
+
+.video-description {
+  color: var(--text-secondary);
+  font-size: 14px;
+  line-height: 1.4;
+  margin-bottom: 12px;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.video-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-bottom: 16px;
+}
+
+.video-tags .tag {
+  background-color: var(--background-color);
+  color: var(--text-secondary);
+  padding: 2px 8px;
+  border-radius: var(--border-radius-sm);
+  font-size: 12px;
+  border: 1px solid var(--border-color);
+}
+
+.video-actions {
+  padding: 12px 16px;
+  border-top: 1px solid var(--border-color);
+  display: flex;
+  gap: 8px;
+  justify-content: flex-end;
+  background-color: rgba(29, 29, 31, 0.02);
+}
+
+.video-actions .btn {
+  padding: 6px 12px;
+  font-size: 13px;
+}
+
+/* AI重塑进度样式 */
+.remix-progress {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.8);
+  padding: 8px 12px;
+  color: white;
+}
+
+.remix-progress-bar {
+  width: 100%;
+  height: 4px;
+  background: rgba(255, 255, 255, 0.3);
+  border-radius: 2px;
+  overflow: hidden;
+  margin-bottom: 4px;
+}
+
+.remix-progress-fill {
+  height: 100%;
+  background: var(--primary-color);
+  transition: width 0.3s ease;
+  border-radius: 2px;
+}
+
+.remix-style {
+  font-size: 11px;
+  font-weight: 600;
+  text-align: center;
+}
+
+/* 工具类 */
+.text-primary { color: var(--text-primary); }
+.text-secondary { color: var(--text-secondary); }
+.text-muted { color: var(--text-muted); }
+.text-success { color: var(--success-color); }
+.text-warning { color: var(--warning-color); }
+.text-error { color: var(--error-color); }
+.text-info { color: var(--info-color); }
+
+.bg-primary { background-color: var(--primary-color); }
+.bg-accent { background-color: var(--accent-color); }
+.bg-success { background-color: var(--success-color); }
+.bg-warning { background-color: var(--warning-color); }
+.bg-error { background-color: var(--error-color); }
+.bg-info { background-color: var(--info-color); }
+
+.shadow-sm { box-shadow: var(--shadow-sm); }
+.shadow-md { box-shadow: var(--shadow-md); }
+.shadow-lg { box-shadow: var(--shadow-lg); }
+
+.rounded-sm { border-radius: var(--border-radius-sm); }
+.rounded-md { border-radius: var(--border-radius-md); }
+.rounded-lg { border-radius: var(--border-radius-lg); }
+/* 卡片内容区域固定定位 */
+.card-content {
+  position: absolute;
+  top: 200px;
+  left: 0;
+  right: 0;
+  bottom: 68px;
+  padding: 0 16px;
+  overflow-y: auto; /* 内容超出高度时,自动显示垂直滚动条,可滑动浏览完整内容 */
+}
+/* 卡片按钮区域固定定位 */
+.card-actions {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  min-height: 68px;
+  padding: 8px 16px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 8px;
+  background: rgba(251, 251, 253, 0.96);
+}
+
+/* ===== 视频预览模态框 ===== */
+.preview-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.75);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+  backdrop-filter: blur(4px);
+  animation: fadeIn 0.2s ease;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+.preview-modal {
+  background: #ffffff;
+  border-radius: 18px;
+  width: 92%;
+  max-width: 760px;
+  max-height: 92vh;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.38), 0 8px 24px rgba(0, 0, 0, 0.12);
+  animation: slideUp 0.25s ease;
+  overflow: hidden;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+@keyframes slideUp {
+  from { transform: translateY(20px); opacity: 0; }
+  to { transform: translateY(0); opacity: 1; }
+}
+
+.preview-modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 18px 22px;
+  background: #ffffff;
+  flex-shrink: 0;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
+}
+
+.preview-modal-header h3 {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  color: #1d1d1f;
+  letter-spacing: -0.01em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+  margin-right: 12px;
+}
+
+.preview-close-btn {
+  background: rgba(0, 0, 0, 0.04);
+  border: none;
+  cursor: pointer;
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  color: #6e6e73;
+  transition: all 0.15s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.preview-close-btn:hover {
+  background: rgba(0, 0, 0, 0.08);
+  color: #1d1d1f;
+}
+
+.preview-modal-body {
+  overflow-y: auto;
+  flex: 1;
+  min-height: 0;
+  background: #ffffff;
+}
+
+.preview-player {
+  background: radial-gradient(ellipse at center, #1c1c1e 0%, #000 100%);
+  width: 100%;
+  height: min(56vh, 480px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+
+.preview-video-element {
+  max-width: 100%;
+  max-height: 100%;
+  width: auto;
+  height: 100%;
+  object-fit: contain;
+  background: transparent;
+  border-radius: 0;
+}
+
+.preview-info {
+  padding: 20px 22px 22px;
+  background: #fafafa;
+}
+
+.preview-info-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+  gap: 14px 18px;
+  background: #ffffff;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  border-radius: 12px;
+  padding: 16px 18px;
+}
+
+.preview-info-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  min-width: 0;
+}
+
+.preview-info-label {
+  font-size: 11px;
+  color: #86868b;
+  font-weight: 500;
+  letter-spacing: 0.02em;
+  text-transform: uppercase;
+}
+
+.preview-info-value {
+  font-size: 13.5px;
+  color: #1d1d1f;
+  font-weight: 500;
+  word-break: break-word;
+  line-height: 1.4;
+}
+
+.preview-description {
+  margin-top: 14px;
+  padding: 14px 18px;
+  background: #ffffff;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  border-radius: 12px;
+}
+
+.preview-description .preview-info-label {
+  display: block;
+  margin-bottom: 6px;
+}
+
+.preview-description p {
+  margin: 0;
+  font-size: 13.5px;
+  color: #424245;
+  line-height: 1.6;
+  white-space: pre-wrap;
+}
+
+.preview-tags {
+  margin-top: 14px;
+  padding: 14px 18px;
+  background: #ffffff;
+  border: 1px solid rgba(0, 0, 0, 0.06);
+  border-radius: 12px;
+}
+
+.preview-tags .preview-info-label {
+  display: block;
+  margin-bottom: 8px;
+}
+
+.preview-tag-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-top: 0;
+}
+
+.preview-tag-list .tag {
+  background: rgba(0, 113, 227, 0.1);
+  color: #0071e3;
+  padding: 3px 10px;
+  border-radius: 999px;
+  font-size: 12px;
+  font-weight: 500;
+  line-height: 1.4;
+}
+
+.preview-modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 14px 22px;
+  border-top: 1px solid rgba(0, 0, 0, 0.06);
+  background: #ffffff;
+  flex-shrink: 0;
+}
+
+.preview-modal-footer .btn {
+  text-decoration: none;
+  background: rgba(0, 0, 0, 0.04);
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  color: #1d1d1f;
+  border-radius: 980px;
+  padding: 8px 18px;
+  font-size: 14px;
+  font-weight: 500;
+  min-height: 36px;
+  box-shadow: none;
+  transition: all 0.15s ease;
+}
+
+.preview-modal-footer .btn:hover {
+  background: rgba(0, 0, 0, 0.08);
+  transform: none;
+  box-shadow: none;
+}
+
+.preview-modal-footer .btn.btn-primary,
+.preview-modal-footer a.btn.btn-primary,
+.preview-modal-footer button.btn.btn-primary {
+  background: #0071e3 !important;
+  background-image: none !important;
+  border: 1px solid #0071e3 !important;
+  color: #ffffff !important;
+  box-shadow: 0 4px 12px rgba(0, 113, 227, 0.32) !important;
+}
+
+.preview-modal-footer .btn.btn-primary:hover,
+.preview-modal-footer a.btn.btn-primary:hover,
+.preview-modal-footer button.btn.btn-primary:hover {
+  background: #0077ed !important;
+  border-color: #0077ed !important;
+  filter: none;
+  transform: translateY(-1px);
+  box-shadow: 0 6px 16px rgba(0, 113, 227, 0.4) !important;
+}
+
+.preview-modal-footer .btn.btn-primary:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+  transform: none;
+}
+
+/* ===== 通用模态框 ===== */
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.75);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+  backdrop-filter: blur(4px);
+  animation: fadeIn 0.2s ease;
+}
+
+.modal {
+  background: rgba(251, 251, 253, 0.94);
+  border-radius: var(--border-radius-lg);
+  width: 90%;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.24);
+  animation: slideUp 0.25s ease;
+  overflow: hidden;
+  backdrop-filter: saturate(180%) blur(24px);
+  border: 1px solid rgba(255, 255, 255, 0.72);
+}
+
+.modal .modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  border-bottom: 1px solid var(--border-color);
+}
+
+.modal .modal-header h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--text-primary);
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.modal .modal-body {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.modal .modal-footer {
+  border-top: 1px solid var(--border-color);
+  padding: 12px 20px;
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+}
+
+/* ===== AI重塑模态框 ===== */
+.remix-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.75);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+  backdrop-filter: blur(4px);
+  animation: fadeIn 0.2s ease;
+}
+
+.remix-modal {
+  background: rgba(251, 251, 253, 0.94);
+  border-radius: var(--border-radius-lg);
+  width: 90%;
+  max-width: 760px;
+  max-height: 90vh;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.24);
+  animation: slideUp 0.25s ease;
+  overflow: hidden;
+  backdrop-filter: saturate(180%) blur(24px);
+  border: 1px solid rgba(255, 255, 255, 0.72);
+}
+
+.remix-modal-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  border-bottom: 1px solid var(--border-color);
+}
+
+.remix-modal-header h3 {
+  margin: 0;
+  font-size: 16px;
+  font-weight: 600;
+  color: var(--text-primary);
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+}
+
+.remix-modal-header h3 .icon {
+  color: var(--primary-color);
+}
+
+.remix-modal-body {
+  padding: 20px;
+  overflow-y: auto;
+  flex: 1;
+}
+
+.remix-section {
+  margin-bottom: 18px;
+}
+
+.remix-label {
+  display: block;
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-secondary);
+  margin-bottom: 8px;
+}
+
+.remix-mode-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.remix-mode-card {
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  padding: 14px 16px;
+  background: var(--surface-color);
+  cursor: pointer;
+  transition: all var(--transition-fast);
+}
+
+.remix-mode-card:hover {
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 1px rgba(0, 113, 227, 0.12);
+}
+
+.remix-mode-card.active {
+  border-color: var(--primary-color);
+  background: rgba(0, 113, 227, 0.06);
+  box-shadow: 0 0 0 1px rgba(0, 113, 227, 0.15);
+}
+
+.remix-mode-title {
+  font-size: 14px;
+  font-weight: 700;
+  color: var(--text-primary);
+  margin-bottom: 6px;
+}
+
+.remix-mode-desc {
+  font-size: 12px;
+  line-height: 1.6;
+  color: var(--text-muted);
+}
+
+.remix-style-grid {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 8px;
+}
+
+.remix-style-card {
+  padding: 10px 6px;
+  border: 2px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  text-align: center;
+  cursor: pointer;
+  transition: all var(--transition-fast);
+  background: var(--surface-color);
+}
+
+.remix-style-card:hover {
+  border-color: var(--primary-color);
+  background: rgba(0, 113, 227, 0.06);
+}
+
+.remix-style-card.active {
+  border-color: var(--primary-color);
+  background: rgba(0, 113, 227, 0.08);
+  box-shadow: 0 0 0 1px var(--primary-color);
+}
+
+.remix-style-name {
+  font-size: 13px;
+  font-weight: 500;
+  color: var(--text-primary);
+}
+
+.remix-style-card.active .remix-style-name {
+  color: var(--primary-color);
+  font-weight: 600;
+}
+
+.remix-textarea {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  font-size: 13px;
+  line-height: 1.5;
+  resize: vertical;
+  font-family: inherit;
+  color: var(--text-primary);
+  background: var(--surface-color);
+  transition: border-color var(--transition-fast);
+  box-sizing: border-box;
+}
+
+.remix-textarea:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.12);
+}
+
+.remix-textarea:disabled {
+  background: #f7f7f7;
+  cursor: not-allowed;
+}
+
+.remix-hint {
+  font-size: 12px;
+  color: var(--text-muted);
+  margin-top: 4px;
+}
+
+.remix-digital-human-box {
+  border: 1px dashed rgba(0, 113, 227, 0.28);
+  border-radius: var(--border-radius-md);
+  padding: 16px;
+  background: rgba(0, 113, 227, 0.04);
+}
+
+.remix-digital-human-tip {
+  padding: 10px 12px;
+  border-radius: var(--border-radius-md);
+  background: rgba(0, 113, 227, 0.08);
+  color: var(--primary-dark);
+  border: 1px solid rgba(0, 113, 227, 0.14);
+  font-size: 13px;
+  margin-bottom: 10px;
+}
+
+.remix-digital-human-preview {
+  margin-top: 14px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.remix-digital-human-preview img,
+.remix-digital-human-inline-preview img {
+  width: 120px;
+  height: 120px;
+  object-fit: cover;
+  border-radius: 12px;
+  border: 1px solid var(--border-color);
+  background: #fff;
+  flex-shrink: 0;
+}
+
+.remix-digital-human-meta {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  flex-wrap: wrap;
+}
+
+.remix-digital-human-name {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--text-primary);
+  word-break: break-all;
+}
+
+.remix-digital-human-inline-preview {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 12px 14px;
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  background: #fafbfc;
+}
+
+.remix-params {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 12px;
+  margin-bottom: 16px;
+}
+
+.remix-param-item select {
+  width: 100%;
+  padding: 8px 10px;
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-sm);
+  font-size: 13px;
+  color: var(--text-primary);
+  background: var(--surface-color);
+  cursor: pointer;
+}
+
+.remix-param-item select:disabled {
+  background: #f7f7f7;
+  cursor: not-allowed;
+}
+
+.remix-params-digital-human {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.remix-audio-source-summary {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 12px 14px;
+  border-radius: var(--border-radius-md);
+  border: 1px solid rgba(29, 29, 31, 0.06);
+  background: rgba(29, 29, 31, 0.02);
+  font-size: 13px;
+  color: var(--text-secondary);
+  flex-wrap: wrap;
+}
+
+.remix-audio-source-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.remix-audio-source-card {
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  padding: 14px 16px;
+  background: var(--surface-color);
+  cursor: pointer;
+  transition: all var(--transition-fast);
+}
+
+.remix-audio-source-card:hover {
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 1px rgba(0, 113, 227, 0.12);
+}
+
+.remix-audio-source-card.active {
+  border-color: var(--primary-color);
+  background: rgba(0, 113, 227, 0.06);
+  box-shadow: 0 0 0 1px rgba(0, 113, 227, 0.15);
+}
+
+.remix-voice-picker {
+  margin-top: 14px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.voice-selected-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 12px 14px;
+  border-radius: var(--border-radius-md);
+  border: 1px solid rgba(29, 29, 31, 0.06);
+  background: rgba(29, 29, 31, 0.02);
+  flex-wrap: wrap;
+}
+
+.voice-selected-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: var(--text-primary);
+}
+
+.remix-param-checkbox {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.remix-checkbox-label {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 13px;
+  color: var(--text-primary);
+}
+
+.remix-checkbox-label input {
+  width: 16px;
+  height: 16px;
+}
+
+.remix-cost {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 10px 14px;
+  background: #fffbeb;
+  border: 1px solid #fde68a;
+  border-radius: var(--border-radius-md);
+  font-size: 13px;
+  color: #92400e;
+  margin-bottom: 16px;
+}
+
+.remix-cost .icon {
+  font-size: 18px;
+  color: #d97706;
+}
+
+.remix-cost strong {
+  color: #b45309;
+}
+
+.remix-progress-section {
+  margin-top: 12px;
+}
+
+.remix-progress-bar-container {
+  width: 100%;
+  height: 8px;
+  background: var(--border-color);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.remix-progress-bar-fill {
+  height: 100%;
+  background: var(--primary-color);
+  border-radius: 4px;
+  transition: width 0.5s ease;
+}
+
+.remix-progress-text {
+  font-size: 12px;
+  color: var(--text-secondary);
+  margin-top: 6px;
+  text-align: center;
+}
+
+.remix-digital-human-progress-card {
+  margin-top: 14px;
+  padding: 14px;
+  border-radius: var(--border-radius-md);
+  border: 1px solid #c7d2fe;
+  background: #eef2ff;
+}
+
+.remix-modal-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 12px 20px;
+  border-top: 1px solid var(--border-color);
+}
+
+.remix-footer-status {
+  flex: 1;
+  text-align: center;
+  font-size: 13px;
+  color: var(--text-secondary);
+}
+
+/* ===== 步骤指示器 ===== */
+.remix-stepper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  padding: 14px 20px;
+  border-bottom: 1px solid var(--border-color);
+  background: #fafbfc;
+  flex-wrap: wrap;
+}
+
+.remix-step-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 8px;
+  border-radius: 16px;
+  font-size: 12px;
+  color: var(--text-muted);
+  transition: all var(--transition-fast);
+}
+
+.remix-step-item.active {
+  background: var(--primary-color);
+  color: #fff;
+}
+
+.remix-step-item.done {
+  color: var(--success-color);
+}
+
+.remix-step-circle {
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 11px;
+  font-weight: 600;
+  border: 1.5px solid var(--border-color);
+  background: var(--surface-color);
+  flex-shrink: 0;
+}
+
+.remix-step-item.active .remix-step-circle {
+  background: #fff;
+  color: var(--primary-color);
+  border-color: #fff;
+}
+
+.remix-step-item.done .remix-step-circle {
+  background: var(--success-color);
+  color: #fff;
+  border-color: var(--success-color);
+}
+
+.remix-step-text {
+  white-space: nowrap;
+}
+
+/* ===== 原始文字稿展示 ===== */
+.remix-original-text {
+  padding: 10px 12px;
+  background: #f7f8fa;
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  font-size: 13px;
+  line-height: 1.6;
+  color: var(--text-secondary);
+  max-height: 120px;
+  overflow-y: auto;
+  white-space: pre-wrap;
+}
+
+/* ===== 分镜脚本卡片 ===== */
+.storyboard-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.storyboard-card {
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  padding: 12px;
+  background: var(--surface-color);
+  transition: border-color var(--transition-fast);
+}
+
+.storyboard-card.generating {
+  border-color: var(--primary-color);
+  background: #f0fbff;
+}
+
+.storyboard-card.completed {
+  border-color: var(--success-color);
+  background: #f0fdf4;
+}
+
+.storyboard-card.failed {
+  border-color: var(--error-color);
+  background: #fef2f2;
+}
+
+.storyboard-card-head {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 6px;
+}
+
+.history-primary-video {
+  margin-bottom: 18px;
+  padding: 14px;
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-md);
+  background: var(--surface-color);
+}
+
+.history-primary-video-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+}
+
+.history-primary-video-head h4 {
+  margin: 0;
+  font-size: 16px;
+}
+
+.history-primary-video-player {
+  width: 100%;
+  max-height: 360px;
+  border-radius: 8px;
+  background: #000;
+}
+
+.history-detail-loading {
+  margin-bottom: 16px;
+  padding: 12px 14px;
+  border-radius: 8px;
+  background: #f8fafc;
+  color: var(--text-secondary);
+  font-size: 13px;
+}
+
+.storyboard-id {
+  font-size: 12px;
+  font-weight: 700;
+  color: var(--primary-color);
+  background: #fff0f3;
+  padding: 1px 8px;
+  border-radius: 10px;
+}
+
+.storyboard-act {
+  font-size: 12px;
+  color: var(--text-muted);
+}
+
+.storyboard-dur {
+  font-size: 12px;
+  color: var(--text-muted);
+  margin-left: auto;
+}
+
+.storyboard-narration {
+  font-size: 13px;
+  color: var(--text-primary);
+  line-height: 1.5;
+  margin-bottom: 6px;
+}
+
+.storyboard-prompt {
+  font-size: 12px !important;
+  min-height: unset;
+}
+
+.storyboard-error {
+  font-size: 12px;
+  color: var(--error-color);
+  margin-top: 4px;
+}
+
+.storyboard-video-link {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+  color: var(--accent-color);
+  text-decoration: none;
+  margin-top: 4px;
+}
+
+.storyboard-video-link:hover {
+  text-decoration: underline;
+}
+
+/* ===== 结果汇总 ===== */
+.remix-result-summary {
+  display: flex;
+  justify-content: center;
+  gap: 24px;
+  padding: 20px 0;
+}
+
+.remix-result-stat {
+  text-align: center;
+}
+
+.remix-result-num {
+  display: block;
+  font-size: 32px;
+  font-weight: 700;
+}
+
+.remix-result-stat.success .remix-result-num {
+  color: var(--success-color);
+}
+
+.remix-result-stat.failed .remix-result-num {
+  color: var(--error-color);
+}
+
+.remix-result-stat.total .remix-result-num {
+  color: var(--text-primary);
+}
+
+.remix-result-label {
+  font-size: 13px;
+  color: var(--text-muted);
+}
+
+.remix-whisper-loaded {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 10px 14px;
+  background: #f0fdf4;
+  border: 1px solid #86efac;
+  border-radius: var(--border-radius-md);
+  font-size: 13px;
+  color: #166534;
+  margin-bottom: 10px;
+}
+
+.btn-accent {
+  background: var(--surface-dark);
+  color: #fff;
+  border: 1px solid transparent;
+  border-radius: var(--pill-radius);
+  padding: 8px 16px;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+}
+
+.btn-accent:hover {
+  background: #2c2c2e;
+}
+
+/* ===== 一键成片状态横幅 ===== */
+.remix-stitch-banner {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 16px;
+  border-radius: var(--border-radius-md);
+  font-size: 13px;
+  margin-bottom: 12px;
+  background: #eff6ff;
+  border: 1px solid #93c5fd;
+  color: #1e40af;
+}
+
+.remix-stitch-banner.stitch-done {
+  background: #f0fdf4;
+  border-color: #86efac;
+  color: #166534;
+}
+
+.remix-stitch-banner.stitch-failed {
+  background: #fef2f2;
+  border-color: #fca5a5;
+  color: #991b1b;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+.spinning {
+  animation: spin 1s linear infinite;
+}
+
+/* ===== 无文字稿提示区 ===== */
+.remix-no-transcript {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  padding: 32px 16px;
+  border: 2px dashed var(--border-color);
+  border-radius: var(--border-radius-md);
+  margin-bottom: 12px;
+}
+
+.remix-no-transcript-text {
+  font-size: 15px;
+  color: var(--text-muted);
+  font-weight: 500;
+}
+
+.remix-no-transcript-actions {
+  display: flex;
+  gap: 10px;
+  flex-wrap: wrap;
+  justify-content: center;
+}
+
+@media (max-width: 768px) {
+  .remix-mode-grid,
+  .remix-params,
+  .remix-params-digital-human {
+    grid-template-columns: 1fr;
+  }
+
+  .remix-digital-human-inline-preview {
+    align-items: flex-start;
+    flex-direction: column;
+  }
+}
+
+.remix-whisper-progress {
+  width: 100%;
+  max-width: 320px;
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.4; }
+}
+
+/* ==================== Toast 通知 ==================== */
+
+.toast-container {
+  position: fixed;
+  top: 80px;
+  right: 24px;
+  z-index: 10000;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 14px 20px;
+  border-radius: 12px;
+  background: rgba(29, 29, 31, 0.92);
+  border: 1px solid transparent;
+  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
+  cursor: pointer;
+  animation: toastSlideIn 0.3s ease-out;
+  max-width: 420px;
+  min-width: 280px;
+  word-break: break-word;
+  backdrop-filter: saturate(180%) blur(20px);
+}
+
+.toast-success {
+  background: #16a34a;
+  border-color: #16a34a;
+}
+
+.toast-info {
+  background: #2563eb;
+  border-color: #2563eb;
+}
+
+.toast-warn {
+  background: #ea580c;
+  border-color: #ea580c;
+}
+
+.toast-error {
+  background: #dc2626;
+  border-color: #dc2626;
+}
+
+.toast-success .toast-icon,
+.toast-info .toast-icon,
+.toast-warn .toast-icon,
+.toast-error .toast-icon {
+  color: #ffffff;
+}
+
+.toast-text {
+  flex: 1;
+  font-size: 13px;
+  color: #ffffff;
+}
+
+.toast-close {
+  font-size: 16px;
+  color: rgba(255, 255, 255, 0.9);
+  cursor: pointer;
+}
+
+@keyframes toastSlideIn {
+  from { opacity: 0; transform: translateX(60px); }
+  to   { opacity: 1; transform: translateX(0); }
+}
+
+/* ==================== 任务卡片增强 ==================== */
+
+.task-card.task-done {
+  border-left: 3px solid #4CAF50;
+}
+
+.task-card.task-fail {
+  border-left: 3px solid #F44336;
+}
+
+.task-time {
+  color: var(--text-muted);
+  font-size: 12px;
+  margin-left: auto;
+}
+
+.task-detail-info {
+  display: flex;
+  gap: 16px;
+  padding: 6px 0;
+  font-size: 12px;
+  color: var(--text-secondary);
+}
+
+.task-meta {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.task-error {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 12px;
+  margin-top: 6px;
+  border-radius: 6px;
+  background: rgba(244, 67, 54, 0.08);
+  font-size: 12px;
+  color: #F44336;
+}
+
+.task-result {
+  margin-top: 8px;
+}
+
+.btn-sm {
+  padding: 4px 12px;
+  font-size: 12px;
+}
+
+.progress-done {
+  background: #4CAF50 !important;
+}
+
+.progress-fail {
+  background: #F44336 !important;
+}
+
+/* ============================================================
+ * 数字人合成页面(深色紫粉渐变主题)— 仅作用于 .dh-page 子树
+ * ============================================================ */
+.dh-page {
+  /* full-bleed dark surface inside .workbench (which has 28px padding) */
+  margin: -28px;
+  padding: 32px 36px 48px;
+  min-height: calc(100vh - 52px);
+  /* solid base color,便于跨主题平滑插值;gradient 光晕由 ::before / ::after 交叉淡入接管 */
+  position: relative;
+  isolation: isolate;
+  background-color: #0b0d1a;
+  color: #e8eaf2;
+  --dh-text: #e8eaf2;
+  --dh-text-2: rgba(232, 234, 242, 0.65);
+  --dh-text-3: rgba(232, 234, 242, 0.42);
+  --dh-card: rgba(22, 27, 46, 0.78);
+  --dh-card-2: rgba(28, 33, 54, 0.85);
+  --dh-border: rgba(255, 255, 255, 0.08);
+  --dh-border-2: rgba(255, 255, 255, 0.14);
+  --dh-grad: linear-gradient(135deg, #6b59ff 0%, #a855f7 50%, #ec4899 100%);
+  --dh-grad-soft: linear-gradient(135deg, rgba(107, 89, 255, 0.22), rgba(236, 72, 153, 0.18));
+  --dh-success: #34d399;
+  --dh-error: #f87171;
+  /* 重映射全局 token,让旧版 .btn / .form-field / inline-style var() 自动变深 */
+  --background-color: transparent;
+  --surface-color: rgba(22, 27, 46, 0.6);
+  --surface-elevated: rgba(22, 27, 46, 0.78);
+  --bg-primary: rgba(11, 14, 28, 0.6);
+  --bg-secondary: rgba(13, 16, 32, 0.55);
+  --bg-tertiary: rgba(28, 33, 54, 0.85);
+  --text-primary: #e8eaf2;
+  --text-secondary: rgba(232, 234, 242, 0.65);
+  --text-muted: rgba(232, 234, 242, 0.42);
+  --border-color: rgba(255, 255, 255, 0.08);
+  --accent-color: #a855f7;
+  --accent-dark: #8b5cf6;
+  --primary-color: #a855f7;
+  --primary-dark: #7c3aed;
+  --success-color: #34d399;
+  --error-color: #f87171;
+  --warning-color: #fbbf24;
+  --info-color: #60a5fa;
+}
+
+/* ---- 深色壳内的旧组件适配 ---- */
+.dh-page .search-header { padding: 0 0 14px; margin-bottom: 20px; border-bottom: 1px solid var(--dh-border); }
+.dh-page .search-header h2 { color: var(--dh-text); font-size: 18px; font-weight: 600; }
+.dh-page .search-header .subtitle { color: var(--dh-text-2); font-size: 13px; }
+.dh-page .btn {
+  background: rgba(255,255,255,0.05);
+  color: var(--dh-text-2);
+  border: 1px solid var(--dh-border-2);
+  min-height: 36px;
+  padding: 8px 16px;
+  font-size: 13px;
+}
+.dh-page .btn:hover { background: rgba(255,255,255,0.1); color: var(--dh-text); }
+.dh-page .btn.btn-primary {
+  background: var(--dh-grad);
+  color: #fff;
+  border: none;
+  box-shadow: 0 6px 16px rgba(168, 85, 247, 0.32);
+}
+.dh-page .btn.btn-primary:hover {
+  background: var(--dh-grad);
+  transform: translateY(-1px);
+  box-shadow: 0 10px 24px rgba(236, 72, 153, 0.42);
+}
+.dh-page .btn.btn-primary:disabled,
+.dh-page .btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
+.dh-page .btn-sm { padding: 4px 10px; min-height: 28px; font-size: 12px; }
+.dh-page .form-field label {
+  display: block; color: var(--dh-text-2); font-size: 12.5px; font-weight: 500; margin-bottom: 6px;
+}
+.dh-page .form-field input:not([type="range"]):not([type="checkbox"]),
+.dh-page .form-field select,
+.dh-page .form-field textarea {
+  width: 100%;
+  padding: 10px 14px;
+  background: rgba(11, 14, 28, 0.6);
+  border: 1px solid var(--dh-border);
+  border-radius: 10px;
+  color: var(--dh-text);
+  font-size: 13.5px;
+  line-height: 1.55;
+  font-family: inherit;
+  outline: none;
+  transition: border-color 0.18s ease, box-shadow 0.18s ease;
+}
+.dh-page .form-field input:focus,
+.dh-page .form-field select:focus,
+.dh-page .form-field textarea:focus {
+  border-color: rgba(168, 85, 247, 0.55);
+  box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.12);
+}
+.dh-page .form-field select option { background: #161a2e; color: var(--dh-text); }
+.dh-page .form-field input::placeholder,
+.dh-page .form-field textarea::placeholder { color: rgba(232, 234, 242, 0.32); }
+.dh-page input[type="range"] {
+  width: 100%; height: 6px; border-radius: 999px; appearance: none;
+  background: rgba(255,255,255,0.08); outline: none;
+}
+.dh-page input[type="range"]::-webkit-slider-thumb {
+  appearance: none; width: 16px; height: 16px; border-radius: 50%;
+  background: linear-gradient(135deg, #a855f7, #ec4899);
+  border: 2px solid #fff; cursor: pointer; box-shadow: 0 2px 6px rgba(168, 85, 247, 0.5);
+}
+.dh-page code {
+  padding: 2px 6px; border-radius: 4px;
+  background: rgba(168, 85, 247, 0.12); color: #c4b5fd;
+  font-size: 12px;
+}
+.dh-page audio { filter: invert(0.92) hue-rotate(180deg); }
+.dh-page .progress-bar {
+  background: rgba(255,255,255,0.08); border-radius: 999px; overflow: hidden;
+}
+.dh-page .progress-fill {
+  background: var(--dh-grad);
+  box-shadow: 0 0 12px rgba(168, 85, 247, 0.5);
+}
+.dh-page .spinner {
+  width: 32px; height: 32px; border-radius: 50%;
+  border: 3px solid rgba(255,255,255,0.12);
+  border-top-color: #a855f7;
+  animation: dh-spin 0.8s linear infinite;
+}
+/* 兼容原 .remix-mode-card 仅在 voice-synthesis 旧切换时出现(已废弃但保留) */
+.dh-page .remix-mode-card {
+  background: rgba(22, 27, 46, 0.6);
+  border: 1px solid var(--dh-border);
+  border-radius: 12px;
+}
+.dh-page .remix-mode-card.active {
+  border-color: rgba(168, 85, 247, 0.55);
+  background: var(--dh-grad-soft);
+}
+.dh-page .remix-mode-title { color: var(--dh-text); }
+.dh-page .remix-mode-desc { color: var(--dh-text-3); }
+
+.dh-page * { box-sizing: border-box; }
+.dh-page h1, .dh-page h2, .dh-page h3, .dh-page h4 { color: var(--dh-text); margin: 0; }
+.dh-page p { margin: 0; color: var(--dh-text-2); }
+
+/* ---- 顶部页眉 ---- */
+.dh-page-header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 18px 22px;
+  border-radius: 16px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  backdrop-filter: blur(12px);
+  margin-bottom: 22px;
+}
+.dh-header-icon {
+  width: 44px;
+  height: 44px;
+  border-radius: 12px;
+  background: var(--dh-grad);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  box-shadow: 0 8px 24px rgba(124, 58, 237, 0.45);
+  flex-shrink: 0;
+}
+.dh-header-text { flex: 1; min-width: 0; }
+.dh-header-text h1 { font-size: 18px; font-weight: 600; letter-spacing: 0.2px; }
+.dh-header-text p { font-size: 12.5px; margin-top: 2px; color: var(--dh-text-3); }
+.dh-help-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 7px 14px;
+  border-radius: 999px;
+  background: transparent;
+  border: 1px solid var(--dh-border-2);
+  color: var(--dh-text-2);
+  font-size: 13px;
+  cursor: pointer;
+  transition: all 0.18s ease;
+}
+.dh-help-btn:hover { background: rgba(255,255,255,0.06); color: var(--dh-text); }
+.dh-help-icon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 16px;
+  height: 16px;
+  border: 1px solid currentColor;
+  border-radius: 50%;
+  font-size: 11px;
+  font-weight: 600;
+}
+
+/* ---- 状态横幅 ---- */
+.dh-status-banner {
+  margin-bottom: 18px;
+  padding: 16px 20px;
+  border-radius: 14px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  backdrop-filter: blur(12px);
+}
+.dh-status-banner__head {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+.dh-status-banner__text { font-size: 14px; font-weight: 500; color: var(--dh-text); }
+.dh-status-banner__percent { margin-left: auto; font-weight: 600; font-size: 13px; color: var(--dh-text-2); }
+.dh-status-banner__check {
+  width: 22px; height: 22px; border-radius: 50%;
+  background: rgba(52, 211, 153, 0.18); color: var(--dh-success);
+  display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
+}
+.dh-status-banner__cross {
+  width: 22px; height: 22px; border-radius: 50%;
+  background: rgba(248, 113, 113, 0.18); color: var(--dh-error);
+  display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
+}
+.dh-status-banner--error { border-color: rgba(248, 113, 113, 0.35); }
+.dh-status-banner--success { border-color: rgba(52, 211, 153, 0.32); }
+.dh-status-banner__error { margin-top: 8px; font-size: 13px; color: var(--dh-error); }
+.dh-status-banner__meta {
+  margin-top: 12px;
+  display: flex; align-items: center; gap: 12px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  background: rgba(255,255,255,0.03);
+  border: 1px solid var(--dh-border);
+}
+.dh-status-banner__meta img { width: 48px; height: 48px; object-fit: cover; border-radius: 8px; }
+.dh-status-banner__name { font-size: 13.5px; color: var(--dh-text); font-weight: 500; }
+.dh-status-banner__sub { font-size: 12px; color: var(--dh-text-3); margin-top: 2px; }
+.dh-status-banner__video { margin-top: 12px; display: flex; justify-content: center; background: #000; border-radius: 10px; overflow: hidden; }
+.dh-status-banner__video video { max-width: 480px; width: 100%; max-height: 360px; display: block; }
+
+.dh-progress-bar {
+  margin-top: 10px;
+  height: 6px; border-radius: 999px;
+  background: rgba(255,255,255,0.06); overflow: hidden;
+}
+.dh-progress-bar__fill {
+  height: 100%; background: var(--dh-grad);
+  transition: width 0.3s ease; border-radius: 999px;
+  box-shadow: 0 0 12px rgba(168, 85, 247, 0.6);
+}
+.dh-spinner {
+  width: 16px; height: 16px; border-radius: 50%;
+  border: 2px solid rgba(255,255,255,0.2);
+  border-top-color: #a855f7;
+  animation: dh-spin 0.8s linear infinite;
+}
+@keyframes dh-spin { to { transform: rotate(360deg); } }
+
+.dh-link-btn {
+  display: inline-flex; align-items: center; gap: 6px;
+  padding: 6px 12px; border-radius: 999px;
+  font-size: 12.5px; cursor: pointer;
+  background: var(--dh-grad); color: #fff; border: none;
+  text-decoration: none;
+  transition: transform 0.15s ease, box-shadow 0.2s ease;
+}
+.dh-link-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(168, 85, 247, 0.4); }
+.dh-link-btn--ghost {
+  background: transparent; border: 1px solid var(--dh-border-2); color: var(--dh-text-2);
+}
+.dh-link-btn--ghost:hover { background: rgba(255,255,255,0.06); color: var(--dh-text); box-shadow: none; }
+
+/* ---- 主卡片 ---- */
+.dh-card {
+  border-radius: 18px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  backdrop-filter: blur(14px);
+  padding: 24px;
+  margin-bottom: 32px;
+  transition: opacity 0.2s ease;
+}
+.dh-card--locked { opacity: 0.55; pointer-events: none; }
+.dh-card__grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
+  gap: 24px;
+}
+@media (max-width: 1100px) {
+  .dh-card__grid { grid-template-columns: 1fr; }
+}
+
+/* ---- 上传区 ---- */
+.dh-upload {
+  display: flex; flex-direction: column;
+  background: rgba(13, 16, 32, 0.55);
+  border-radius: 14px;
+  min-height: 360px;
+  padding: 6px;
+}
+.dh-upload__zone {
+  flex: 1;
+  display: flex; flex-direction: column; align-items: center; justify-content: center;
+  text-align: center;
+  gap: 14px;
+  padding: 36px 28px;
+  border: 1.5px dashed rgba(168, 85, 247, 0.35);
+  border-radius: 12px;
+  cursor: pointer;
+  transition: border-color 0.2s ease, background 0.2s ease;
+}
+.dh-upload__zone:hover {
+  border-color: rgba(236, 72, 153, 0.55);
+  background: rgba(168, 85, 247, 0.04);
+}
+.dh-upload__icon {
+  width: 56px; height: 56px;
+  padding: 12px; border-radius: 14px;
+  background: var(--dh-grad-soft);
+  color: #c4b5fd;
+}
+.dh-upload__title { font-size: 15px; font-weight: 500; color: var(--dh-text); }
+.dh-upload__hints {
+  list-style: none; padding: 0; margin: 0;
+  display: flex; flex-direction: column; gap: 6px;
+  max-width: 480px;
+}
+.dh-upload__hints li {
+  font-size: 12px; color: var(--dh-text-3); line-height: 1.6;
+}
+.dh-upload__preview {
+  flex: 1;
+  display: grid;
+  grid-template-columns: 1fr;
+  grid-template-rows: 1fr auto auto;
+  gap: 10px;
+  padding: 12px;
+}
+.dh-upload__preview img {
+  width: 100%; height: 100%; min-height: 220px; object-fit: cover;
+  border-radius: 10px; background: #000;
+}
+.dh-upload__preview-meta { display: flex; flex-direction: column; gap: 2px; padding: 0 4px; }
+.dh-upload__preview-name { font-size: 14px; font-weight: 500; color: var(--dh-text); word-break: break-all; }
+.dh-upload__preview-size { font-size: 12px; color: var(--dh-text-3); }
+.dh-upload__preview-actions { display: flex; gap: 8px; padding: 0 4px 4px; }
+
+.dh-chip-btn {
+  display: inline-flex; align-items: center; gap: 4px;
+  padding: 6px 12px; border-radius: 8px; cursor: pointer;
+  background: rgba(255,255,255,0.06); border: 1px solid var(--dh-border);
+  color: var(--dh-text-2); font-size: 12px;
+  transition: background 0.15s ease, color 0.15s ease;
+}
+.dh-chip-btn:hover { background: rgba(255,255,255,0.1); color: var(--dh-text); }
+.dh-chip-btn--danger:hover { background: rgba(248, 113, 113, 0.15); color: var(--dh-error); border-color: rgba(248, 113, 113, 0.35); }
+
+/* ---- 表单 ---- */
+.dh-form { display: flex; flex-direction: column; gap: 16px; }
+.dh-field { display: flex; flex-direction: column; gap: 8px; }
+.dh-field__label {
+  display: flex; justify-content: space-between; align-items: center;
+  font-size: 13px; color: var(--dh-text); font-weight: 500;
+}
+.dh-field__label em { color: var(--dh-error); font-style: normal; margin-left: 2px; }
+.dh-field__counter { font-size: 11.5px; color: var(--dh-text-3); font-weight: 400; }
+.dh-field-row { display: flex; gap: 14px; align-items: flex-end; flex-wrap: wrap; }
+
+.dh-input,
+.dh-textarea {
+  width: 100%;
+  padding: 10px 14px;
+  background: rgba(11, 14, 28, 0.6);
+  border: 1px solid var(--dh-border);
+  border-radius: 10px;
+  color: var(--dh-text);
+  font-size: 13.5px;
+  line-height: 1.55;
+  font-family: inherit;
+  outline: none;
+  transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
+}
+.dh-input::placeholder,
+.dh-textarea::placeholder { color: rgba(232, 234, 242, 0.32); }
+.dh-input:focus,
+.dh-textarea:focus {
+  border-color: rgba(168, 85, 247, 0.55);
+  box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.12);
+  background: rgba(11, 14, 28, 0.85);
+}
+.dh-textarea { resize: vertical; min-height: 88px; }
+.dh-input[type="text"] { min-height: 40px; }
+select.dh-input {
+  appearance: none;
+  background-image: linear-gradient(45deg, transparent 50%, var(--dh-text-2) 50%),
+                    linear-gradient(135deg, var(--dh-text-2) 50%, transparent 50%);
+  background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
+  background-size: 5px 5px, 5px 5px;
+  background-repeat: no-repeat;
+  padding-right: 32px;
+  cursor: pointer;
+}
+select.dh-input option { background: #161a2e; color: var(--dh-text); }
+
+/* segmented control */
+.dh-segment {
+  display: inline-flex;
+  padding: 4px;
+  border-radius: 12px;
+  background: rgba(11, 14, 28, 0.65);
+  border: 1px solid var(--dh-border);
+  gap: 2px;
+}
+.dh-segment__item {
+  flex: 1;
+  padding: 8px 18px;
+  background: transparent; border: none; cursor: pointer;
+  color: var(--dh-text-2);
+  border-radius: 8px;
+  font-size: 13px;
+  transition: all 0.18s ease;
+}
+.dh-segment__item:hover { color: var(--dh-text); }
+.dh-segment__item.is-active {
+  background: var(--dh-grad);
+  color: #fff;
+  box-shadow: 0 6px 18px rgba(168, 85, 247, 0.35);
+}
+
+/* chips */
+.dh-chips { display: flex; flex-wrap: wrap; gap: 6px; }
+.dh-chip {
+  padding: 5px 12px; border-radius: 999px; cursor: pointer;
+  background: rgba(255,255,255,0.05);
+  border: 1px solid var(--dh-border);
+  color: var(--dh-text-2); font-size: 12px;
+  transition: all 0.15s ease;
+}
+.dh-chip:hover { color: var(--dh-text); background: rgba(255,255,255,0.08); }
+.dh-chip.is-active {
+  background: var(--dh-grad-soft);
+  border-color: rgba(168, 85, 247, 0.55);
+  color: #fff;
+}
+
+/* 上传音频小区 */
+.dh-mini-drop {
+  display: flex; align-items: center; justify-content: center;
+  flex-direction: column; gap: 4px;
+  text-align: center;
+  padding: 20px;
+  border: 1.5px dashed rgba(168, 85, 247, 0.3);
+  border-radius: 10px;
+  background: rgba(11, 14, 28, 0.5);
+  cursor: pointer;
+  transition: border-color 0.18s ease, background 0.18s ease;
+}
+.dh-mini-drop:hover { border-color: rgba(236, 72, 153, 0.5); background: rgba(168, 85, 247, 0.05); }
+.dh-mini-drop__title { font-size: 13px; color: var(--dh-text); font-weight: 500; }
+.dh-mini-drop__hint { font-size: 11.5px; color: var(--dh-text-3); }
+.dh-mini-drop--filled {
+  flex-direction: row; justify-content: space-between;
+  cursor: default;
+  border-style: solid;
+  border-color: rgba(168, 85, 247, 0.45);
+  background: rgba(168, 85, 247, 0.06);
+  text-align: left;
+}
+.dh-mini-drop__name { font-size: 13px; color: var(--dh-text); font-weight: 500; word-break: break-all; }
+.dh-mini-drop__sub { font-size: 11.5px; color: var(--dh-text-3); margin-top: 2px; }
+.dh-mini-drop__actions { display: flex; gap: 6px; flex-shrink: 0; }
+
+/* 开关 */
+.dh-toggle {
+  display: inline-flex; align-items: center; gap: 10px; cursor: pointer;
+  user-select: none; padding: 8px 4px;
+}
+.dh-toggle input { position: absolute; opacity: 0; pointer-events: none; }
+.dh-toggle__track {
+  position: relative; width: 38px; height: 22px; border-radius: 999px;
+  background: rgba(255,255,255,0.12);
+  transition: background 0.2s ease;
+}
+.dh-toggle__thumb {
+  position: absolute; top: 3px; left: 3px;
+  width: 16px; height: 16px; border-radius: 50%;
+  background: #fff;
+  transition: left 0.2s ease, background 0.2s ease;
+}
+.dh-toggle input:checked + .dh-toggle__track { background: var(--dh-grad); }
+.dh-toggle input:checked + .dh-toggle__track .dh-toggle__thumb { left: 19px; }
+.dh-toggle__label { display: flex; flex-direction: column; font-size: 13px; color: var(--dh-text); }
+.dh-toggle__label small { font-size: 11px; color: var(--dh-text-3); margin-top: 1px; }
+
+.dh-error {
+  padding: 10px 14px; border-radius: 10px;
+  background: rgba(248, 113, 113, 0.1);
+  border: 1px solid rgba(248, 113, 113, 0.3);
+  color: var(--dh-error); font-size: 12.5px;
+}
+
+/* 主 CTA 按钮 */
+.dh-cta {
+  margin-top: 4px;
+  display: flex; align-items: center; justify-content: center; gap: 10px;
+  width: 100%;
+  padding: 14px 22px;
+  border: none; border-radius: 12px; cursor: pointer;
+  font-size: 15px; font-weight: 600; color: #fff;
+  background: var(--dh-grad);
+  box-shadow: 0 10px 30px rgba(168, 85, 247, 0.4);
+  transition: transform 0.15s ease, box-shadow 0.2s ease, opacity 0.15s ease;
+}
+.dh-cta:hover:not(:disabled) {
+  transform: translateY(-1px);
+  box-shadow: 0 14px 38px rgba(236, 72, 153, 0.5);
+}
+.dh-cta:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
+
+/* ---- 形象库 ---- */
+.dh-gallery {
+  border-top: 1px solid var(--dh-border);
+  padding-top: 24px;
+}
+.dh-gallery__header {
+  display: flex; align-items: flex-end; justify-content: space-between;
+  gap: 16px; margin-bottom: 18px; flex-wrap: wrap;
+}
+.dh-gallery__header h2 { font-size: 18px; font-weight: 600; }
+.dh-gallery__header p { font-size: 12.5px; color: var(--dh-text-3); margin-top: 4px; }
+.dh-refresh-btn {
+  display: inline-flex; align-items: center; gap: 6px;
+  padding: 8px 16px; border-radius: 999px; cursor: pointer;
+  background: transparent; border: 1px solid var(--dh-border-2);
+  color: var(--dh-text-2); font-size: 13px;
+  transition: all 0.18s ease;
+}
+.dh-refresh-btn:hover { background: rgba(255,255,255,0.06); color: var(--dh-text); }
+.dh-refresh-icon { font-size: 15px; line-height: 1; }
+
+.dh-gallery__empty {
+  display: flex; flex-direction: column; align-items: center; gap: 10px;
+  padding: 56px 20px;
+  border-radius: 14px;
+  background: var(--dh-card);
+  border: 1px dashed var(--dh-border-2);
+  color: var(--dh-text-3); font-size: 13px;
+}
+.dh-gallery__empty-icon { font-size: 32px; opacity: 0.7; }
+
+.dh-gallery__grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+  gap: 16px;
+}
+.dh-gallery__card {
+  background: var(--dh-card-2);
+  border: 1px solid var(--dh-border);
+  border-radius: 14px;
+  overflow: hidden;
+  display: flex; flex-direction: column;
+  transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
+}
+.dh-gallery__card:hover {
+  transform: translateY(-2px);
+  border-color: rgba(168, 85, 247, 0.45);
+  box-shadow: 0 14px 36px rgba(0, 0, 0, 0.45);
+}
+.dh-gallery__media {
+  position: relative;
+  aspect-ratio: 3 / 4;
+  background: #000;
+  overflow: hidden;
+}
+.dh-gallery__media video {
+  width: 100%; height: 100%; object-fit: cover; display: block;
+}
+.dh-gallery__play {
+  position: absolute; inset: 0;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 28px; color: #fff; text-decoration: none;
+  background: linear-gradient(180deg, rgba(0,0,0,0) 50%, rgba(0,0,0,0.55) 100%);
+  opacity: 0;
+  transition: opacity 0.2s ease;
+}
+.dh-gallery__card:hover .dh-gallery__play { opacity: 1; }
+.dh-gallery__body {
+  padding: 12px 14px 14px;
+  display: flex; flex-direction: column; gap: 8px;
+}
+.dh-gallery__title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+.dh-gallery__title {
+  font-size: 13px; font-weight: 500;
+  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+  flex: 1; min-width: 0;
+}
+.dh-gallery__icons { display: flex; gap: 4px; flex-shrink: 0; }
+.dh-gallery__icon-btn {
+  width: 24px; height: 24px;
+  display: inline-flex; align-items: center; justify-content: center;
+  border: none; cursor: pointer; border-radius: 6px;
+  background: transparent; color: var(--dh-text-3);
+  font-size: 13px; text-decoration: none;
+  transition: background 0.15s ease, color 0.15s ease;
+}
+.dh-gallery__icon-btn:hover { background: rgba(255,255,255,0.08); color: var(--dh-text); }
+.dh-gallery__icon-btn--danger:hover { background: rgba(248, 113, 113, 0.15); color: var(--dh-error); }
+.dh-gallery__info {
+  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px;
+  font-size: 11px; color: var(--dh-text-3);
+}
+.dh-gallery__chip {
+  margin-left: auto;
+  padding: 2px 8px; border-radius: 4px;
+  background: rgba(168, 85, 247, 0.18);
+  color: #c4b5fd;
+  font-size: 10px; font-weight: 600; letter-spacing: 0.6px;
+}
+
+/* ============================================================
+ * 语音合成页面(voice-synthesis):二栏布局,复用 .dh-* 组件
+ * ============================================================ */
+.vs-two-col {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+  gap: 20px;
+  align-items: start;
+  margin-bottom: 24px;
+}
+@media (max-width: 1100px) {
+  .vs-two-col { grid-template-columns: 1fr; }
+}
+.vs-subcard {
+  display: flex; flex-direction: column; gap: 14px;
+  padding: 22px;
+}
+.vs-subcard-header {
+  display: flex; align-items: flex-start; gap: 12px;
+  padding-bottom: 14px;
+  border-bottom: 1px solid var(--dh-border);
+  margin-bottom: 4px;
+}
+.vs-subcard-icon {
+  width: 38px; height: 38px; border-radius: 10px;
+  display: flex; align-items: center; justify-content: center;
+  color: #fff; flex-shrink: 0;
+  box-shadow: 0 6px 18px rgba(124, 58, 237, 0.35);
+}
+.vs-subcard-icon--purple { background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); }
+.vs-subcard-icon--pink { background: linear-gradient(135deg, #a855f7 0%, #ec4899 100%); box-shadow: 0 6px 18px rgba(236, 72, 153, 0.35); }
+.vs-subcard-header h2 { font-size: 16px; font-weight: 600; }
+.vs-subcard-header p { font-size: 12px; color: var(--dh-text-3); margin-top: 2px; }
+.vs-subcard-header > div:not(.vs-subcard-icon) { flex: 1; min-width: 0; }
+.vs-subcard-header .dh-help-btn { padding: 4px 10px; font-size: 12px; flex-shrink: 0; }
+
+.vs-tab-header { align-self: stretch; }
+.vs-tab-header .dh-segment__item:disabled { opacity: 0.4; cursor: not-allowed; }
+
+.vs-audio-upload {
+  min-height: 180px;
+  padding: 24px 20px;
+}
+.vs-audio-upload .dh-upload__title { font-size: 13.5px; }
+
+.vs-upload-filled {
+  display: flex; align-items: center; justify-content: space-between;
+  gap: 12px;
+  padding: 14px 16px;
+  border: 1px solid rgba(168, 85, 247, 0.45);
+  background: rgba(168, 85, 247, 0.08);
+  border-radius: 10px;
+}
+
+.vs-hint { font-size: 11.5px; color: var(--dh-text-3); line-height: 1.55; margin: 0; }
+
+.vs-select-row { display: flex; gap: 8px; align-items: stretch; }
+.vs-select-row .dh-input { flex: 1; }
+.vs-preview-btn {
+  width: 40px; flex-shrink: 0;
+  border: 1px solid var(--dh-border-2);
+  background: rgba(255,255,255,0.05);
+  color: var(--dh-text-2);
+  border-radius: 999px;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.15s ease;
+}
+.vs-preview-btn:hover { background: rgba(168, 85, 247, 0.2); color: #fff; border-color: rgba(168, 85, 247, 0.5); }
+
+.vs-result {
+  margin-top: 4px; padding: 14px 16px; border-radius: 12px;
+  background: rgba(52, 211, 153, 0.08);
+  border: 1px solid rgba(52, 211, 153, 0.3);
+}
+.vs-result-head {
+  display: flex; align-items: center; gap: 8px;
+  font-size: 13px; font-weight: 600; color: var(--dh-text);
+  margin-bottom: 10px;
+}
+.vs-result-info { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; color: var(--dh-text-2); }
+.vs-result-info strong { color: var(--dh-text); font-weight: 500; margin-right: 4px; }
+.vs-audio-compact { height: 30px; vertical-align: middle; }
+.vs-audio-full { width: 100%; margin: 6px 0 8px; }
+
+.vs-cloned-list { margin-top: 4px; padding-top: 14px; border-top: 1px solid var(--dh-border); }
+.vs-subtle-title { font-size: 12px; color: var(--dh-text-3); font-weight: 500; margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.5px; }
+
+.vs-api-footer {
+  margin-top: 8px; padding: 18px 20px; border-radius: 14px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  font-size: 12.5px; color: var(--dh-text-2);
+}
+.vs-api-footer h4 { color: var(--dh-text); font-size: 13px; font-weight: 600; margin-bottom: 10px; }
+.vs-api-footer > div { margin-top: 4px; line-height: 1.7; }
+.vs-api-footer strong { color: var(--dh-text); font-weight: 500; }
+
+/* ============================================================
+ * 视频生成工作流(video-generation):在 .dh-page 内的微调
+ * ============================================================ */
+.vg-step-bar {
+  display: flex; align-items: center; gap: 4px;
+  margin: 0 0 24px; padding: 18px 22px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 14px;
+  overflow-x: auto;
+}
+.vg-step-item { display: flex; flex-direction: column; align-items: center; min-width: 90px; }
+.vg-step-badge {
+  width: 34px; height: 34px; border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 13px; font-weight: 600; color: #fff; margin-bottom: 6px;
+  background: rgba(255,255,255,0.08); color: var(--dh-text-3);
+  transition: all 0.2s ease;
+}
+.vg-step-item.is-done .vg-step-badge {
+  background: linear-gradient(135deg, #10b981, #34d399);
+  color: #fff;
+}
+.vg-step-item.is-active .vg-step-badge {
+  background: var(--dh-grad);
+  color: #fff;
+  box-shadow: 0 0 0 4px rgba(168, 85, 247, 0.2), 0 6px 16px rgba(168, 85, 247, 0.4);
+}
+.vg-step-label { font-size: 12px; color: var(--dh-text-3); white-space: nowrap; }
+.vg-step-item.is-active .vg-step-label,
+.vg-step-item.is-done .vg-step-label { color: var(--dh-text); }
+.vg-step-connector {
+  flex: 1; height: 2px; min-width: 20px;
+  background: rgba(255,255,255,0.08);
+  transition: background 0.2s ease;
+}
+.vg-step-connector.is-done { background: linear-gradient(90deg, #34d399, #a855f7); }
+
+/* ---- 步骤卡片瀑布(始终全部可见) ---- */
+.vg-step-card {
+  margin-bottom: 16px;
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 16px;
+  overflow: hidden;
+  transition: border-color 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
+}
+.vg-step-card[data-state="active"] {
+  border-color: rgba(168, 85, 247, 0.45);
+  box-shadow: 0 12px 32px rgba(124, 58, 237, 0.18), 0 0 0 1px rgba(168, 85, 247, 0.25) inset;
+}
+.vg-step-card[data-state="done"] {
+  border-color: rgba(52, 211, 153, 0.25);
+}
+.vg-step-card[data-state="locked"] {
+  opacity: 0.55;
+}
+
+.vg-step-card__header {
+  display: flex; align-items: center; gap: 14px;
+  padding: 16px 20px;
+  cursor: pointer;
+  user-select: none;
+}
+.vg-step-card[data-state="locked"] .vg-step-card__header { cursor: not-allowed; }
+.vg-step-card[data-state="active"] .vg-step-card__header { cursor: default; }
+.vg-step-card.is-collapsed .vg-step-card__header { padding: 14px 20px; }
+.vg-step-card[data-state="done"] .vg-step-card__header:hover {
+  background: rgba(255, 255, 255, 0.03);
+}
+
+.vg-step-card__num {
+  width: 36px; height: 36px; border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+  font-size: 14px; font-weight: 700; color: #fff; flex-shrink: 0;
+  background: rgba(255, 255, 255, 0.08); color: var(--dh-text-3);
+}
+.vg-step-card[data-state="active"] .vg-step-card__num {
+  background: var(--dh-grad);
+  color: #fff;
+  box-shadow: 0 6px 16px rgba(168, 85, 247, 0.4);
+}
+.vg-step-card[data-state="done"] .vg-step-card__num {
+  background: linear-gradient(135deg, #10b981, #34d399);
+  color: #fff;
+}
+
+.vg-step-card__meta { flex: 1; min-width: 0; }
+.vg-step-card__meta h3 {
+  font-size: 15px; font-weight: 600; color: var(--dh-text);
+  margin: 0 0 2px; line-height: 1.3;
+}
+.vg-step-card__meta p {
+  font-size: 12px; color: var(--dh-text-3); margin: 0; line-height: 1.4;
+}
+
+.vg-step-card__chip {
+  padding: 4px 10px; border-radius: 999px;
+  font-size: 11px; font-weight: 600; letter-spacing: 0.3px;
+  flex-shrink: 0;
+}
+.vg-step-card__chip[data-state="done"] {
+  background: rgba(52, 211, 153, 0.14); color: #6ee7b7;
+}
+.vg-step-card__chip[data-state="active"] {
+  background: rgba(168, 85, 247, 0.18); color: #d8b4fe;
+}
+.vg-step-card__chip[data-state="locked"] {
+  background: rgba(255, 255, 255, 0.06); color: var(--dh-text-3);
+}
+
+.vg-step-card__edit {
+  padding: 5px 12px; border-radius: 999px;
+  background: rgba(255, 255, 255, 0.06);
+  border: 1px solid var(--dh-border-2);
+  color: var(--dh-text-2);
+  font-size: 12px; font-weight: 500;
+  cursor: pointer; flex-shrink: 0;
+  transition: all 0.15s ease;
+}
+.vg-step-card__edit:hover {
+  background: rgba(168, 85, 247, 0.18);
+  color: #fff;
+  border-color: rgba(168, 85, 247, 0.5);
+}
+
+.vg-step-card__toggle {
+  width: 30px; height: 30px; border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+  background: rgba(255, 255, 255, 0.05);
+  border: 1px solid var(--dh-border-2);
+  color: var(--dh-text-2);
+  cursor: pointer; flex-shrink: 0;
+  transition: transform 0.2s ease, background 0.15s ease;
+}
+.vg-step-card__toggle.is-disabled { opacity: 0.4; cursor: not-allowed; }
+.vg-step-card:not(.is-collapsed) .vg-step-card__toggle:not(.is-disabled) {
+  transform: rotate(180deg);
+}
+
+.vg-step-card__body {
+  padding: 4px 22px 22px;
+  border-top: 1px solid var(--dh-border);
+  margin-top: 0;
+  animation: vg-step-slide 0.2s ease;
+}
+@keyframes vg-step-slide {
+  from { opacity: 0; transform: translateY(-4px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+.vg-step-card__body > *:first-child { margin-top: 16px; }
+
+/* 使 footer 使用说明也用统一深色卡片 */
+.vg-page > section[ng-reflect-ng-if] + div[style*="使用说明"],
+.vg-help-card {
+  background: var(--dh-card) !important;
+  border: 1px solid var(--dh-border) !important;
+  border-radius: 14px !important;
+}
+
+/* ============================================================
+ * 全局深色紫粉主题(覆盖原 Apple 浅色变量)
+ * 让所有 tab 共享同一套色板,单页面再叠 .dh-page xx-page 做精修
+ * ============================================================ */
+:root {
+  --primary-color: #a855f7;
+  --primary-dark: #7c3aed;
+  --accent-color: #ec4899;
+  --accent-dark: #db2777;
+  --background-color: #0f0a1f;
+  --surface-color: #1a1230;
+  --surface-elevated: rgba(30, 22, 55, 0.85);
+  --surface-dark: #0f0a1f;
+  --surface-dark-soft: #1a1230;
+  --text-primary: #f5f3ff;
+  --text-secondary: rgba(245, 243, 255, 0.72);
+  --text-muted: rgba(245, 243, 255, 0.45);
+  --border-color: rgba(168, 85, 247, 0.18);
+  --success-color: #34d399;
+  --warning-color: #fbbf24;
+  --error-color: #f87171;
+  --info-color: #a855f7;
+  --shadow-sm: 0 10px 30px rgba(0, 0, 0, 0.35);
+  --shadow-md: 0 18px 50px rgba(0, 0, 0, 0.5);
+  --shadow-lg: 0 24px 60px rgba(0, 0, 0, 0.6);
+  --app-grad: linear-gradient(135deg, #a855f7 0%, #ec4899 100%);
+}
+
+/* 主体背景:深紫渐变铺底 + 顶部光晕 */
+html, body {
+  background-color: var(--background-color);
+  color: var(--text-primary);
+  margin: 0;
+  padding: 0;
+  height: 100%;
+  overflow: hidden; /* 防止 body 滚动,所有滚动交给 .workbench 内部处理 */
+}
+.app-container {
+  /* 注意:原规则已设 height: 100vh,这里仅追加背景;切勿再加 min-height 否则会撑出 body 滚动条 */
+  position: relative;
+  isolation: isolate; /* 形成独立 stacking context,让 ::before/::after z-index:-1 留在容器内 */
+  background-color: #0f0a1f; /* 夜间底色(可被 background-color 平滑插值,不闪) */
+  overflow: hidden;
+}
+/* 夜间渐变光晕层(默认可见) */
+.app-container::before {
+  content: '';
+  position: absolute;
+  inset: 0;
+  z-index: -1;
+  pointer-events: none;
+  background:
+    radial-gradient(circle at 0% 0%, rgba(124, 58, 237, 0.18), transparent 45%),
+    radial-gradient(circle at 100% 0%, rgba(236, 72, 153, 0.12), transparent 50%),
+    radial-gradient(circle at 50% 100%, rgba(99, 102, 241, 0.10), transparent 60%);
+  opacity: 1;
+  transition: opacity 0.5s ease;
+}
+/* 日间渐变光晕层(默认隐藏) */
+.app-container::after {
+  content: '';
+  position: absolute;
+  inset: 0;
+  z-index: -1;
+  pointer-events: none;
+  background:
+    radial-gradient(circle at 0% 0%, rgba(59, 130, 246, 0.08), transparent 45%),
+    radial-gradient(circle at 100% 0%, rgba(6, 182, 212, 0.06), transparent 50%),
+    radial-gradient(circle at 50% 100%, rgba(99, 102, 241, 0.05), transparent 60%);
+  opacity: 0;
+  transition: opacity 0.5s ease;
+}
+.app-container.theme-day::before { opacity: 0; }
+.app-container.theme-day::after { opacity: 1; }
+
+/* ===== 顶部 Header ===== */
+.header {
+  /* solid color 替代 linear-gradient,便于跨主题平滑插值;视觉效果几乎相同 */
+  background: rgba(16, 11, 33, 0.94) !important;
+  border-bottom: none !important;
+  /* 双层阴影:内侧紫色高光(与下方内容渐隐过渡)+ 外投影 */
+  box-shadow:
+    inset 0 -1px 0 rgba(168, 85, 247, 0.18),
+    0 12px 32px -16px rgba(0, 0, 0, 0.8),
+    0 1px 0 rgba(168, 85, 247, 0.08);
+  backdrop-filter: saturate(180%) blur(24px);
+}
+.header .logo {
+  /* 改用 solid 亮色 + 紫色发光,避免 gradient text 在浅色叠底时不可见 */
+  color: #f5f3ff !important;
+  -webkit-text-fill-color: #f5f3ff;
+  background: none !important;
+  font-weight: 700;
+  letter-spacing: 0.5px;
+  text-shadow: 0 0 18px rgba(168, 85, 247, 0.5);
+}
+.header .logo small {
+  color: rgba(245, 243, 255, 0.6) !important;
+  -webkit-text-fill-color: rgba(245, 243, 255, 0.6);
+  background: none !important;
+  font-weight: 400;
+  margin-left: 6px;
+  text-shadow: none;
+}
+
+/* ===== 侧边栏 ===== */
+.sidebar {
+  /* solid color 便于平滑插值(去掉 180deg 上下渐变) */
+  background: rgba(18, 12, 37, 0.93);
+  border-right: none;
+  /* 内侧紫色发光带 + 渐隐右侧阴影,做软过渡到主内容 */
+  box-shadow:
+    inset -1px 0 0 rgba(168, 85, 247, 0.15),
+    12px 0 32px -16px rgba(0, 0, 0, 0.6);
+  backdrop-filter: saturate(180%) blur(20px);
+  padding: 16px 0;
+  position: relative;
+}
+/* 右边缘的紫色渐隐线 - 增强与主内容的视觉过渡 */
+.sidebar::after {
+  content: '';
+  position: absolute;
+  top: 0; right: -1px; bottom: 0;
+  width: 1px;
+  background: linear-gradient(180deg,
+    transparent 0%,
+    rgba(168, 85, 247, 0.25) 30%,
+    rgba(236, 72, 153, 0.2) 70%,
+    transparent 100%);
+  pointer-events: none;
+}
+
+.nav-list { padding: 0 12px; }
+
+.nav-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin: 4px 0;
+  padding: 11px 14px;
+  border-radius: 12px;
+  color: rgba(245, 243, 255, 0.62);
+  cursor: pointer;
+  transition: all 0.18s ease;
+  font-weight: 500;
+  font-size: 14px;
+  position: relative;
+}
+.nav-item:hover {
+  background: rgba(168, 85, 247, 0.1);
+  color: #f5f3ff;
+}
+.nav-item.active {
+  background: linear-gradient(135deg, rgba(168, 85, 247, 0.22) 0%, rgba(236, 72, 153, 0.18) 100%);
+  color: #fff;
+  box-shadow: 0 4px 16px rgba(124, 58, 237, 0.25), inset 0 0 0 1px rgba(168, 85, 247, 0.35);
+}
+.nav-item.active::before {
+  content: '';
+  position: absolute;
+  left: -12px; top: 8px; bottom: 8px;
+  width: 3px; border-radius: 0 3px 3px 0;
+  background: var(--app-grad);
+}
+.nav-item-divider {
+  height: 1px;
+  margin: 12px 14px;
+  background: rgba(168, 85, 247, 0.15);
+}
+.nav-text { font-size: 14px; }
+
+/* SVG 图标在侧边栏内:保持 currentColor + 固定尺寸 */
+.nav-item .nav-svg {
+  width: 18px; height: 18px;
+  flex-shrink: 0;
+  stroke: currentColor;
+  fill: none;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+/* ===== 主内容区 ===== */
+.workbench {
+  background: transparent;
+  /* 保持 padding: 28px 对称,与 .dh-page 的 margin: -28px full-bleed 对齐,避免横向溢出 */
+  padding: 28px;
+}
+
+/* ===== 通用 section-card(兼容尚未重构的页面)===== */
+.section-card {
+  background: rgba(30, 22, 55, 0.5);
+  border: 1px solid rgba(168, 85, 247, 0.15);
+  border-radius: 18px;
+  padding: 28px;
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
+  backdrop-filter: blur(12px);
+  margin-bottom: 22px;
+}
+.section-card .search-header h2 {
+  color: var(--text-primary) !important;
+  font-size: 26px !important;
+  letter-spacing: -0.01em;
+}
+.section-card .search-header .subtitle {
+  color: var(--text-secondary) !important;
+}
+
+/* ===== 全局按钮(dark) ===== */
+.btn {
+  background: rgba(255, 255, 255, 0.05);
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  color: var(--text-primary);
+  border-radius: 10px;
+}
+.btn:hover {
+  background: rgba(168, 85, 247, 0.15);
+  border-color: rgba(168, 85, 247, 0.4);
+  transform: translateY(-1px);
+  box-shadow: 0 6px 16px rgba(124, 58, 237, 0.2);
+}
+.btn-primary,
+.btn.btn-primary {
+  background: var(--app-grad) !important;
+  border: none !important;
+  color: #fff !important;
+  box-shadow: 0 6px 18px rgba(168, 85, 247, 0.4);
+}
+.btn-primary:hover,
+.btn.btn-primary:hover {
+  filter: brightness(1.1);
+  transform: translateY(-1px);
+  box-shadow: 0 10px 24px rgba(168, 85, 247, 0.5);
+}
+.btn:disabled, .btn[disabled] {
+  opacity: 0.4; cursor: not-allowed; transform: none !important;
+}
+
+/* ===== 表单控件(dark) ===== */
+.form-field label {
+  color: var(--text-secondary);
+}
+.form-field input,
+.form-field select,
+.form-field textarea,
+input[type="text"]:not(.dh-input),
+input[type="search"]:not(.dh-input),
+input[type="number"]:not(.dh-input),
+select:not(.dh-input),
+textarea:not(.dh-textarea) {
+  background: rgba(15, 10, 31, 0.5) !important;
+  border: 1px solid rgba(168, 85, 247, 0.2) !important;
+  color: var(--text-primary) !important;
+}
+.form-field input:focus,
+.form-field select:focus,
+.form-field textarea:focus,
+input:focus:not(.dh-input),
+select:focus:not(.dh-input),
+textarea:focus:not(.dh-textarea) {
+  border-color: rgba(168, 85, 247, 0.6) !important;
+  box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.15) !important;
+  outline: none;
+}
+.form-field select option {
+  background: #1a1230;
+  color: var(--text-primary);
+}
+
+/* ===== 空状态、加载、提示 ===== */
+.empty-state {
+  background: rgba(30, 22, 55, 0.4);
+  border: 1px dashed rgba(168, 85, 247, 0.25);
+  border-radius: 16px;
+  color: var(--text-secondary);
+}
+.empty-state h3 { color: var(--text-primary); }
+.empty-state .icon.large,
+.empty-state span.icon {
+  background: var(--app-grad);
+  -webkit-background-clip: text;
+  background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+
+/* ===== Modal 对话框(dark) ===== */
+.modal-overlay,
+.preview-modal-overlay,
+.remix-modal-overlay {
+  background: rgba(0, 0, 0, 0.65) !important;
+  backdrop-filter: blur(8px);
+}
+.modal,
+.preview-modal,
+.remix-modal {
+  background: var(--surface-color) !important;
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  color: var(--text-primary);
+  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
+}
+.modal-header,
+.preview-modal-header,
+.remix-modal-header {
+  border-bottom: 1px solid rgba(168, 85, 247, 0.15);
+}
+.modal-footer,
+.remix-modal-footer {
+  border-top: 1px solid rgba(168, 85, 247, 0.15);
+}
+
+/* ===== 滚动条(dark) ===== */
+.workbench::-webkit-scrollbar,
+.modal-body::-webkit-scrollbar,
+.preview-modal-body::-webkit-scrollbar,
+.remix-modal-body::-webkit-scrollbar {
+  width: 8px;
+}
+.workbench::-webkit-scrollbar-track,
+.modal-body::-webkit-scrollbar-track,
+.preview-modal-body::-webkit-scrollbar-track,
+.remix-modal-body::-webkit-scrollbar-track {
+  background: transparent;
+}
+.workbench::-webkit-scrollbar-thumb,
+.modal-body::-webkit-scrollbar-thumb,
+.preview-modal-body::-webkit-scrollbar-thumb,
+.remix-modal-body::-webkit-scrollbar-thumb {
+  background: rgba(168, 85, 247, 0.3);
+  border-radius: 4px;
+}
+.workbench::-webkit-scrollbar-thumb:hover,
+.modal-body::-webkit-scrollbar-thumb:hover,
+.preview-modal-body::-webkit-scrollbar-thumb:hover,
+.remix-modal-body::-webkit-scrollbar-thumb:hover {
+  background: rgba(168, 85, 247, 0.5);
+}
+
+/* ===== 进度条 ===== */
+.progress-bar {
+  background: rgba(168, 85, 247, 0.12) !important;
+}
+.progress-bar .progress-fill {
+  background: var(--app-grad) !important;
+}
+
+/* ===== Spinner ===== */
+.spinner {
+  border-color: rgba(168, 85, 247, 0.18) !important;
+  border-top-color: #a855f7 !important;
+}
+
+/* code 标签默认样式 */
+code {
+  background: rgba(168, 85, 247, 0.12);
+  color: #c4b5fd;
+  padding: 1px 6px;
+  border-radius: 4px;
+  font-size: 0.92em;
+}
+
+/* 回归: 在 .dh-page 内变量已被局部重映射,本规则不会破坏既有页 */
+
+/* ============================================================
+ * 视频发现页(search-page):在 .dh-page 内修正局部硬编码颜色
+ * ============================================================ */
+.search-page .search-controls {
+  background: rgba(20, 14, 42, 0.5);
+  border: 1px solid rgba(168, 85, 247, 0.15);
+  border-radius: 16px;
+  padding: 20px;
+  margin-bottom: 24px;
+  backdrop-filter: blur(12px);
+}
+.search-page .video-card {
+  background: rgba(30, 22, 55, 0.6) !important;
+  border: 1px solid rgba(168, 85, 247, 0.12) !important;
+  border-radius: 14px !important;
+}
+.search-page .video-card:hover {
+  border-color: rgba(168, 85, 247, 0.3) !important;
+  box-shadow: 0 16px 36px rgba(124, 58, 237, 0.25) !important;
+}
+.search-page .video-meta-chip {
+  background: rgba(168, 85, 247, 0.12) !important;
+  color: #d8b4fe !important;
+}
+.search-page .video-author-inline {
+  color: #f5f3ff !important;
+}
+.search-page .video-id {
+  color: var(--text-muted) !important;
+}
+.search-page .tag-list .tag,
+.search-page .video-tags .tag {
+  background: rgba(236, 72, 153, 0.12) !important;
+  color: #f9a8d4 !important;
+  border: none;
+}
+.search-page .video-duration {
+  background: rgba(0, 0, 0, 0.7) !important;
+  border: 1px solid rgba(168, 85, 247, 0.3);
+}
+.search-page .interaction-stat {
+  color: var(--text-secondary) !important;
+}
+.search-page .results-header h3 {
+  color: var(--text-primary);
+  font-size: 17px;
+  font-weight: 600;
+  letter-spacing: -0.01em;
+}
+.search-page .empty-state {
+  padding: 60px 24px;
+}
+.search-page .loading-state {
+  text-align: center;
+  padding: 40px;
+  color: var(--text-secondary);
+}
+.search-page .loading-state .spinner {
+  margin: 0 auto 12px;
+}
+.search-page .download-progress {
+  padding: 8px 14px 14px;
+}
+
+/* ============================================================
+ * 博主监测(monitor-page)
+ * ============================================================ */
+.monitor-page .monitor-toolbar-row {
+  display: flex; align-items: center; justify-content: flex-start;
+  margin-bottom: 24px;
+}
+.monitor-page .monitor-toolbar-row > div:empty { display: none; }
+.monitor-page .monitor-toolbar-row > .monitor-toolbar { width: 100%; }
+.monitor-page .monitor-toolbar {
+  display: flex; gap: 12px; align-items: flex-end;
+  background: rgba(20, 14, 42, 0.5);
+  border: 1px solid rgba(168, 85, 247, 0.15);
+  border-radius: 14px;
+  padding: 14px 16px;
+  flex: 1; max-width: 720px;
+}
+.monitor-page .monitor-error {
+  margin-bottom: 18px; padding: 12px 16px;
+  background: rgba(248, 113, 113, 0.1);
+  border: 1px solid rgba(248, 113, 113, 0.25);
+  border-radius: 10px;
+  color: #fca5a5;
+  font-size: 13px;
+}
+.monitor-page .monitor-summary {
+  display: flex; gap: 14px; flex-wrap: wrap;
+  margin-bottom: 22px;
+}
+.monitor-page .monitor-summary-card {
+  flex: 1; min-width: 180px;
+  padding: 16px 20px; border-radius: 14px;
+  background: linear-gradient(135deg, rgba(168, 85, 247, 0.12), rgba(236, 72, 153, 0.08));
+  border: 1px solid rgba(168, 85, 247, 0.2);
+  display: flex; flex-direction: column; gap: 4px;
+}
+.monitor-page .monitor-summary-label {
+  font-size: 12px; color: var(--text-secondary);
+  text-transform: uppercase; letter-spacing: 0.5px;
+}
+.monitor-page .monitor-summary-card strong {
+  font-size: 28px; font-weight: 700;
+  color: #fff !important;
+  -webkit-text-fill-color: #fff;
+  background: none;
+  text-shadow: 0 0 16px rgba(168, 85, 247, 0.55);
+  letter-spacing: -0.01em;
+}
+.monitor-page .monitor-author-card {
+  background: rgba(30, 22, 55, 0.6) !important;
+  border: 1px solid rgba(168, 85, 247, 0.15) !important;
+  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
+}
+.monitor-page .monitor-author-card:hover {
+  border-color: rgba(168, 85, 247, 0.4) !important;
+  box-shadow: 0 18px 42px rgba(124, 58, 237, 0.3);
+}
+.monitor-page .monitor-author-avatar {
+  border: 2px solid rgba(168, 85, 247, 0.4) !important;
+}
+.monitor-page .monitor-author-stat {
+  background: rgba(168, 85, 247, 0.08) !important;
+  border: 1px solid rgba(168, 85, 247, 0.12);
+  border-radius: 10px;
+  padding: 10px 14px;
+}
+.monitor-page .monitor-author-preview img {
+  border: 1px solid rgba(168, 85, 247, 0.15);
+}
+
+/* ============================================================
+ * 任务管理(tasks-page)
+ * ============================================================ */
+.tasks-page .tasks-overview {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+  gap: 16px;
+  margin-bottom: 28px;
+}
+.tasks-page .overview-card {
+  background: rgba(30, 22, 55, 0.6) !important;
+  border: 1px solid rgba(168, 85, 247, 0.15) !important;
+  border-radius: 14px;
+  padding: 18px 20px !important;
+  text-align: left;
+}
+.tasks-page .overview-number {
+  color: #fff !important;
+  -webkit-text-fill-color: #fff;
+  background: none;
+  font-weight: 700;
+  text-shadow: 0 0 14px rgba(168, 85, 247, 0.5);
+}
+.tasks-page .task-card {
+  background: rgba(30, 22, 55, 0.6) !important;
+  border: 1px solid rgba(168, 85, 247, 0.15) !important;
+  border-radius: 14px !important;
+  margin-bottom: 16px;
+}
+
+/* ============================================================
+ * 生成历史(history-page)
+ * ============================================================ */
+.history-page .history-toolbar-row {
+  display: flex; justify-content: flex-end; margin-bottom: 22px;
+}
+.history-page .history-controls {
+  display: flex; gap: 12px; align-items: flex-end;
+  background: rgba(20, 14, 42, 0.5);
+  border: 1px solid rgba(168, 85, 247, 0.15);
+  border-radius: 14px;
+  padding: 14px 16px;
+}
+.history-page .history-record,
+.history-page .history-card,
+.history-page .history-item {
+  background: rgba(30, 22, 55, 0.5) !important;
+  border: 1px solid rgba(168, 85, 247, 0.12) !important;
+  border-radius: 12px;
+}
+
+/* ============================================================
+ * 结果展示(results-page)
+ * ============================================================ */
+.results-page .results-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  gap: 18px;
+}
+.results-page .result-card {
+  background: rgba(30, 22, 55, 0.6) !important;
+  border: 1px solid rgba(168, 85, 247, 0.15) !important;
+  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3) !important;
+}
+.results-page .result-card:hover {
+  border-color: rgba(168, 85, 247, 0.4) !important;
+  box-shadow: 0 18px 42px rgba(124, 58, 237, 0.3) !important;
+}
+.results-page .result-badge {
+  background: rgba(0, 0, 0, 0.6);
+  border: 1px solid rgba(168, 85, 247, 0.3);
+  color: #d8b4fe;
+}
+
+/* ============================================================
+ * 视频管理(videos-page)
+ * ============================================================ */
+.videos-page .videos-toolbar-row {
+  display: flex; gap: 12px; flex-wrap: wrap;
+  align-items: flex-end;
+  background: rgba(20, 14, 42, 0.5);
+  border: 1px solid rgba(168, 85, 247, 0.15);
+  border-radius: 14px;
+  padding: 14px 16px;
+  margin-bottom: 22px;
+}
+.videos-page .videos-controls {
+  display: flex; gap: 12px; flex: 1; flex-wrap: wrap;
+}
+.videos-page .video-management-card,
+.videos-page .video-item {
+  background: rgba(30, 22, 55, 0.6) !important;
+  border: 1px solid rgba(168, 85, 247, 0.15) !important;
+  border-radius: 14px;
+}
+
+/* ============================================================
+ * 全局兜底:让任何残留的 .icon span(48px 残片场景)改用 ::before 占位
+ * 防止 Material Icons 文字裸露
+ * ============================================================ */
+.dh-page .icon[style*="font-size:48px"],
+.dh-page .icon[style*="font-size: 48px"],
+.dh-page .icon.large {
+  font-size: 0 !important;
+}
+.dh-page .empty-state span.icon::before,
+.dh-page .empty-state span.icon.large::before {
+  content: '⊘';
+  font-size: 48px;
+  background: var(--app-grad);
+  -webkit-background-clip: text; background-clip: text;
+  -webkit-text-fill-color: transparent;
+}
+
+/* ============================================================
+ * 主题切换按钮(右上角,圆形纯图标)
+ * ============================================================ */
+.theme-toggle {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 34px;
+  height: 34px;
+  padding: 0;
+  border-radius: 50%;
+  border: 1px solid rgba(255, 255, 255, 0.18);
+  background: rgba(255, 255, 255, 0.08);
+  color: rgba(255, 255, 255, 0.95);
+  cursor: pointer;
+  margin-right: 12px;
+  user-select: none;
+  /* 主题切换时按钮自身也参与过渡 */
+  transition: background-color 0.4s ease, border-color 0.4s ease, color 0.4s ease, transform 0.18s ease, box-shadow 0.18s ease;
+}
+.theme-toggle:hover {
+  background: rgba(255, 255, 255, 0.16);
+  border-color: rgba(255, 255, 255, 0.28);
+  transform: translateY(-1px) rotate(15deg);
+}
+.theme-toggle:active {
+  transform: translateY(0) rotate(0deg);
+}
+.theme-toggle svg {
+  display: block;
+  transition: transform 0.4s ease;
+}
+
+/* ============================================================
+ * 主题切换全局平滑过渡
+ * 给会随主题改变颜色/背景的元素统一加 transition
+ * 注意:transform / opacity 走各自既有的快 transition,避免冲突
+ * ============================================================ */
+.app-container,
+.app-container .header,
+.app-container .sidebar,
+.app-container .sidebar::after,
+.app-container .nav-item,
+.app-container .nav-item::before,
+.app-container .nav-item-divider,
+.app-container .workbench,
+.app-container .section-card,
+.app-container .btn,
+.app-container .form-field input,
+.app-container .form-field select,
+.app-container .form-field textarea,
+.app-container input[type="text"]:not(.dh-input),
+.app-container input[type="search"]:not(.dh-input),
+.app-container input[type="number"]:not(.dh-input),
+.app-container select:not(.dh-input),
+.app-container textarea:not(.dh-textarea),
+.app-container .empty-state,
+.app-container .modal,
+.app-container .preview-modal,
+.app-container .remix-modal,
+.app-container .dh-page,
+.app-container .dh-page-header,
+.app-container .dh-card,
+.app-container .vg-step-card,
+.app-container .vg-help-card,
+.app-container .vs-api-footer,
+.app-container .vg-step-bar,
+.app-container .dh-status-banner,
+.app-container .video-card,
+.app-container .result-card,
+.app-container .monitor-author-card,
+.app-container .monitor-summary-card,
+.app-container .overview-card,
+.app-container .task-card,
+.app-container .history-record,
+.app-container .history-card,
+.app-container .history-item,
+.app-container .video-management-card,
+.app-container .video-item,
+.app-container .search-controls,
+.app-container .monitor-toolbar,
+.app-container .history-controls,
+.app-container .videos-toolbar-row,
+.app-container .video-meta-chip,
+.app-container .tag,
+.app-container code,
+.app-container .progress-bar,
+.app-container .progress-bar .progress-fill {
+  transition:
+    background-color 0.4s ease,
+    background 0.4s ease,
+    border-color 0.4s ease,
+    color 0.4s ease,
+    box-shadow 0.4s ease;
+}
+/* 顶栏 logo / 数字大字 */
+.app-container .header .logo,
+.app-container .header .logo small,
+.app-container .monitor-summary-card strong,
+.app-container .overview-number {
+  transition: color 0.4s ease, text-shadow 0.4s ease;
+}
+
+/* ============================================================
+ * 日间模式(浅蓝白主题)— 默认主题
+ * 通过 .app-container.theme-day 比 flat 暗紫规则提高一级特异性,胜出
+ * ============================================================ */
+.app-container.theme-day {
+  /* 调色板覆盖:蓝白系 */
+  --primary-color: #2563eb;
+  --primary-dark: #1d4ed8;
+  --accent-color: #06b6d4;
+  --accent-dark: #0891b2;
+  --background-color: #f1f5fb;
+  --surface-color: #ffffff;
+  --surface-elevated: #ffffff;
+  --surface-dark: #f8fafc;
+  --surface-dark-soft: #eef2f7;
+  --text-primary: #0f172a;
+  --text-secondary: rgba(15, 23, 42, 0.7);
+  --text-muted: rgba(15, 23, 42, 0.45);
+  --border-color: rgba(37, 99, 235, 0.18);
+  --shadow-sm: 0 4px 12px rgba(15, 23, 42, 0.06);
+  --shadow-md: 0 12px 28px rgba(15, 23, 42, 0.1);
+  --shadow-lg: 0 20px 48px rgba(15, 23, 42, 0.14);
+  --app-grad: linear-gradient(135deg, #3b82f6 0%, #06b6d4 100%);
+
+  /* 主体底色:浅蓝白(gradient 光晕已由 .app-container::after 交叉淡入接管) */
+  background-color: #f1f5fb;
+}
+
+/* ===== 顶栏(日间)===== */
+.app-container.theme-day .header {
+  background: rgba(252, 253, 255, 0.94) !important;
+  box-shadow:
+    inset 0 -1px 0 rgba(37, 99, 235, 0.12),
+    0 8px 24px -16px rgba(15, 23, 42, 0.15) !important;
+  color: var(--text-primary) !important;
+  border-bottom: none !important;
+}
+.app-container.theme-day .header .logo {
+  color: #1e3a8a !important;
+  -webkit-text-fill-color: #1e3a8a;
+  background: none !important;
+  text-shadow: none;
+}
+.app-container.theme-day .header .logo small {
+  color: rgba(15, 23, 42, 0.5) !important;
+  -webkit-text-fill-color: rgba(15, 23, 42, 0.5);
+}
+.app-container.theme-day .theme-toggle {
+  background: rgba(37, 99, 235, 0.08);
+  border-color: rgba(37, 99, 235, 0.25);
+  color: #1e3a8a;
+}
+.app-container.theme-day .theme-toggle:hover {
+  background: rgba(37, 99, 235, 0.16);
+  border-color: rgba(37, 99, 235, 0.4);
+}
+
+/* ===== 侧栏(日间)===== */
+.app-container.theme-day .sidebar {
+  background: rgba(252, 253, 255, 0.94);
+  box-shadow:
+    inset -1px 0 0 rgba(37, 99, 235, 0.1),
+    8px 0 24px -16px rgba(15, 23, 42, 0.08);
+}
+.app-container.theme-day .sidebar::after {
+  background: linear-gradient(180deg,
+    transparent 0%,
+    rgba(59, 130, 246, 0.25) 30%,
+    rgba(6, 182, 212, 0.2) 70%,
+    transparent 100%);
+}
+.app-container.theme-day .nav-item {
+  color: rgba(15, 23, 42, 0.62);
+}
+.app-container.theme-day .nav-item:hover {
+  background: rgba(37, 99, 235, 0.08);
+  color: #1e3a8a;
+}
+.app-container.theme-day .nav-item.active {
+  background: linear-gradient(135deg, rgba(59, 130, 246, 0.16) 0%, rgba(6, 182, 212, 0.12) 100%);
+  color: #1e3a8a;
+  box-shadow: 0 4px 14px rgba(59, 130, 246, 0.18), inset 0 0 0 1px rgba(37, 99, 235, 0.25);
+}
+.app-container.theme-day .nav-item.active::before {
+  background: linear-gradient(135deg, #3b82f6, #06b6d4);
+}
+.app-container.theme-day .nav-item-divider {
+  background: rgba(37, 99, 235, 0.12);
+}
+
+/* ===== 通用 section-card(日间)===== */
+.app-container.theme-day .section-card {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.12) !important;
+  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
+  backdrop-filter: none;
+}
+
+/* ===== 按钮(日间)===== */
+.app-container.theme-day .btn {
+  background: #ffffff;
+  border: 1px solid rgba(37, 99, 235, 0.2);
+  color: var(--text-primary);
+}
+.app-container.theme-day .btn:hover {
+  background: rgba(37, 99, 235, 0.06);
+  border-color: rgba(37, 99, 235, 0.4);
+  box-shadow: 0 6px 16px rgba(59, 130, 246, 0.18);
+}
+.app-container.theme-day .btn-primary,
+.app-container.theme-day .btn.btn-primary {
+  background: linear-gradient(135deg, #3b82f6, #06b6d4) !important;
+  color: #fff !important;
+  box-shadow: 0 6px 18px rgba(59, 130, 246, 0.35);
+}
+
+/* ===== 表单(日间)===== */
+.app-container.theme-day .form-field input,
+.app-container.theme-day .form-field select,
+.app-container.theme-day .form-field textarea,
+.app-container.theme-day input[type="text"]:not(.dh-input),
+.app-container.theme-day input[type="search"]:not(.dh-input),
+.app-container.theme-day input[type="number"]:not(.dh-input),
+.app-container.theme-day select:not(.dh-input),
+.app-container.theme-day textarea:not(.dh-textarea) {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.2) !important;
+  color: var(--text-primary) !important;
+}
+.app-container.theme-day .form-field input:focus,
+.app-container.theme-day .form-field select:focus,
+.app-container.theme-day .form-field textarea:focus,
+.app-container.theme-day input:focus:not(.dh-input),
+.app-container.theme-day select:focus:not(.dh-input),
+.app-container.theme-day textarea:focus:not(.dh-textarea) {
+  border-color: #3b82f6 !important;
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15) !important;
+}
+.app-container.theme-day .form-field select option {
+  background: #ffffff;
+  color: var(--text-primary);
+}
+
+/* ===== 空状态、Modal、滚动条、code(日间)===== */
+.app-container.theme-day .empty-state {
+  background: #ffffff;
+  border: 1px dashed rgba(37, 99, 235, 0.25);
+  color: var(--text-secondary);
+}
+.app-container.theme-day .modal,
+.app-container.theme-day .preview-modal,
+.app-container.theme-day .remix-modal {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.15);
+  color: var(--text-primary);
+  box-shadow: 0 30px 80px rgba(15, 23, 42, 0.18);
+}
+.app-container.theme-day .modal-overlay,
+.app-container.theme-day .preview-modal-overlay,
+.app-container.theme-day .remix-modal-overlay {
+  background: rgba(15, 23, 42, 0.4) !important;
+}
+.app-container.theme-day .workbench::-webkit-scrollbar-thumb,
+.app-container.theme-day .modal-body::-webkit-scrollbar-thumb,
+.app-container.theme-day .preview-modal-body::-webkit-scrollbar-thumb,
+.app-container.theme-day .remix-modal-body::-webkit-scrollbar-thumb {
+  background: rgba(37, 99, 235, 0.3);
+}
+.app-container.theme-day code {
+  background: rgba(37, 99, 235, 0.08);
+  color: #1e40af;
+}
+.app-container.theme-day .progress-bar {
+  background: rgba(37, 99, 235, 0.1) !important;
+}
+.app-container.theme-day .progress-bar .progress-fill {
+  background: linear-gradient(135deg, #3b82f6, #06b6d4) !important;
+}
+.app-container.theme-day .spinner {
+  border-color: rgba(37, 99, 235, 0.18) !important;
+  border-top-color: #3b82f6 !important;
+}
+
+/* ===== 视频发现页(日间)===== */
+.app-container.theme-day .search-page .search-controls {
+  background: #ffffff;
+  border: 1px solid rgba(37, 99, 235, 0.12);
+  backdrop-filter: none;
+}
+.app-container.theme-day .search-page .video-card {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.1) !important;
+  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.06) !important;
+}
+.app-container.theme-day .search-page .video-card:hover {
+  border-color: rgba(37, 99, 235, 0.3) !important;
+  box-shadow: 0 14px 32px rgba(59, 130, 246, 0.18) !important;
+}
+.app-container.theme-day .search-page .video-meta-chip {
+  background: rgba(37, 99, 235, 0.08) !important;
+  color: #1e40af !important;
+}
+.app-container.theme-day .search-page .video-author-inline {
+  color: var(--text-primary) !important;
+}
+.app-container.theme-day .search-page .tag-list .tag,
+.app-container.theme-day .search-page .video-tags .tag {
+  background: rgba(6, 182, 212, 0.12) !important;
+  color: #0e7490 !important;
+}
+.app-container.theme-day .search-page .video-duration {
+  background: rgba(15, 23, 42, 0.78) !important;
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  color: #fff;
+}
+
+/* ===== 博主监测页(日间)===== */
+.app-container.theme-day .monitor-page .monitor-toolbar,
+.app-container.theme-day .monitor-page .history-controls,
+.app-container.theme-day .history-page .history-controls,
+.app-container.theme-day .videos-page .videos-toolbar-row {
+  background: #ffffff;
+  border: 1px solid rgba(37, 99, 235, 0.12);
+}
+.app-container.theme-day .monitor-page .monitor-summary-card {
+  background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(6, 182, 212, 0.06));
+  border: 1px solid rgba(37, 99, 235, 0.18);
+}
+.app-container.theme-day .monitor-page .monitor-summary-card strong {
+  color: #1e3a8a !important;
+  -webkit-text-fill-color: #1e3a8a;
+  text-shadow: none;
+}
+.app-container.theme-day .monitor-page .monitor-error {
+  background: rgba(248, 113, 113, 0.08);
+  border: 1px solid rgba(248, 113, 113, 0.3);
+  color: #b91c1c;
+}
+.app-container.theme-day .monitor-page .monitor-author-card {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.12) !important;
+  box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06);
+}
+.app-container.theme-day .monitor-page .monitor-author-card:hover {
+  border-color: rgba(37, 99, 235, 0.35) !important;
+  box-shadow: 0 14px 32px rgba(59, 130, 246, 0.18);
+}
+.app-container.theme-day .monitor-page .monitor-author-avatar {
+  border: 2px solid rgba(37, 99, 235, 0.3) !important;
+}
+.app-container.theme-day .monitor-page .monitor-author-stat {
+  background: rgba(37, 99, 235, 0.06) !important;
+  border: 1px solid rgba(37, 99, 235, 0.12);
+}
+
+/* ===== 任务/历史/结果/视频管理(日间通用卡片)===== */
+.app-container.theme-day .tasks-page .overview-card,
+.app-container.theme-day .tasks-page .task-card,
+.app-container.theme-day .history-page .history-record,
+.app-container.theme-day .history-page .history-card,
+.app-container.theme-day .history-page .history-item,
+.app-container.theme-day .results-page .result-card,
+.app-container.theme-day .videos-page .video-management-card,
+.app-container.theme-day .videos-page .video-item {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.12) !important;
+  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
+}
+.app-container.theme-day .tasks-page .overview-number {
+  color: #1e3a8a !important;
+  -webkit-text-fill-color: #1e3a8a;
+  text-shadow: none;
+}
+.app-container.theme-day .results-page .result-badge {
+  background: rgba(15, 23, 42, 0.7);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  color: #fff;
+}
+
+/* ===== .dh-page(数字人/语音/视频生成)整体改浅 ===== */
+.app-container.theme-day .dh-page {
+  background-color: #f5f8fc;
+  color: #0f172a;
+  /* 重映射 dh-* tokens 到浅色 */
+  --dh-text: #0f172a;
+  --dh-text-2: rgba(15, 23, 42, 0.7);
+  --dh-text-3: rgba(15, 23, 42, 0.45);
+  --dh-card: rgba(255, 255, 255, 0.85);
+  --dh-card-2: #ffffff;
+  --dh-border: rgba(37, 99, 235, 0.14);
+  --dh-border-2: rgba(37, 99, 235, 0.22);
+  --dh-grad: linear-gradient(135deg, #3b82f6 0%, #06b6d4 100%);
+  --dh-grad-soft: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(6, 182, 212, 0.08));
+  /* 同步全局 token */
+  --background-color: transparent;
+  --surface-color: rgba(255, 255, 255, 0.85);
+  --surface-elevated: #ffffff;
+  --text-primary: #0f172a;
+  --text-secondary: rgba(15, 23, 42, 0.7);
+  --text-muted: rgba(15, 23, 42, 0.45);
+  --border-color: rgba(37, 99, 235, 0.14);
+  /* 这三个 bg-* token 被 inline style 大量引用(如视频生成步骤里的 dashed 上传区)
+   * 必须在 day 模式下显式重映射,否则保留 .dh-page 默认夜间深色 */
+  --bg-primary: rgba(255, 255, 255, 0.85);
+  --bg-secondary: rgba(241, 245, 251, 0.6);
+  --bg-tertiary: #ffffff;
+}
+.app-container.theme-day .dh-page-header {
+  background: rgba(255, 255, 255, 0.85);
+  border: 1px solid rgba(37, 99, 235, 0.14);
+}
+.app-container.theme-day .dh-header-icon {
+  background: linear-gradient(135deg, #3b82f6, #06b6d4);
+  box-shadow: 0 8px 22px rgba(59, 130, 246, 0.32);
+}
+.app-container.theme-day .dh-card,
+.app-container.theme-day .vg-step-card,
+.app-container.theme-day .vg-help-card,
+.app-container.theme-day .vs-api-footer,
+.app-container.theme-day .vg-step-bar {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.12) !important;
+  box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
+}
+.app-container.theme-day .dh-status-banner {
+  background: rgba(255, 255, 255, 0.9);
+  border: 1px solid rgba(37, 99, 235, 0.14);
+}
+.app-container.theme-day .dh-page select.dh-input option,
+.app-container.theme-day .dh-page .form-field select option {
+  background: #ffffff;
+  color: #0f172a;
+}
+
+/* dh-page 内的输入控件需要重新打浅色(.dh-input 在 dh-page scope 内做了暗色边框,需覆盖) */
+.app-container.theme-day .dh-page .dh-input,
+.app-container.theme-day .dh-page .dh-textarea,
+.app-container.theme-day .dh-page .form-field input,
+.app-container.theme-day .dh-page .form-field select,
+.app-container.theme-day .dh-page .form-field textarea {
+  background: #ffffff !important;
+  border: 1px solid rgba(37, 99, 235, 0.18) !important;
+  color: #0f172a !important;
+}
+.app-container.theme-day .dh-page .dh-input:focus,
+.app-container.theme-day .dh-page .dh-textarea:focus,
+.app-container.theme-day .dh-page .form-field input:focus,
+.app-container.theme-day .dh-page .form-field select:focus,
+.app-container.theme-day .dh-page .form-field textarea:focus {
+  border-color: #3b82f6 !important;
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15) !important;
+}
+
+/* ============================================================
+ * Placeholder 颜色(夜间淡白 / 日间中灰),保证两种主题下都清晰可读
+ * ============================================================ */
+/* 夜间(默认) */
+.app-container .form-field input::placeholder,
+.app-container .form-field textarea::placeholder,
+.app-container input::placeholder,
+.app-container textarea::placeholder {
+  color: rgba(245, 243, 255, 0.45);
+}
+/* 日间 */
+.app-container.theme-day .form-field input::placeholder,
+.app-container.theme-day .form-field textarea::placeholder,
+.app-container.theme-day input::placeholder,
+.app-container.theme-day textarea::placeholder,
+.app-container.theme-day .dh-page .form-field input::placeholder,
+.app-container.theme-day .dh-page .form-field textarea::placeholder,
+.app-container.theme-day .dh-input::placeholder,
+.app-container.theme-day .dh-textarea::placeholder {
+  color: rgba(15, 23, 42, 0.42);
+}
+
+/* ============================================================
+ * 上传区(.dh-upload)日间适配
+ * 原始硬编码深色,需要在日间模式覆盖为浅色卡片 + 浅蓝虚线
+ * ============================================================ */
+.app-container.theme-day .dh-upload {
+  background: rgba(241, 245, 251, 0.6);
+}
+.app-container.theme-day .dh-upload__zone {
+  border-color: rgba(37, 99, 235, 0.3);
+  background: rgba(255, 255, 255, 0.4);
+}
+.app-container.theme-day .dh-upload__zone:hover {
+  border-color: rgba(6, 182, 212, 0.55);
+  background: rgba(59, 130, 246, 0.06);
+}
+.app-container.theme-day .dh-upload__icon {
+  background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(6, 182, 212, 0.1));
+  color: #2563eb;
+}
+.app-container.theme-day .dh-upload__title {
+  color: #0f172a;
+}
+.app-container.theme-day .dh-upload__hints li {
+  color: rgba(15, 23, 42, 0.55);
+}
+.app-container.theme-day .dh-upload__preview-name {
+  color: #0f172a;
+}
+.app-container.theme-day .dh-upload__preview-size {
+  color: rgba(15, 23, 42, 0.5);
+}
+.app-container.theme-day .dh-upload__preview img {
+  background: #f1f5fb;
+}
+
+/* ===== 同款适配:语音合成的音频上传 / 数字人内的迷你 drop zone ===== */
+.app-container.theme-day .vs-audio-upload {
+  background: rgba(241, 245, 251, 0.6);
+}
+.app-container.theme-day .dh-mini-drop {
+  background: rgba(255, 255, 255, 0.6) !important;
+  border-color: rgba(37, 99, 235, 0.3) !important;
+  color: rgba(15, 23, 42, 0.7) !important;
+}
+.app-container.theme-day .dh-mini-drop:hover {
+  border-color: rgba(6, 182, 212, 0.5) !important;
+  background: rgba(59, 130, 246, 0.06) !important;
+}
+
+/* ============================================================
+ * .dh-page 双层 gradient 光晕(与 .app-container 同款交叉淡入)
+ * 避免主题切换时 background 在两套 gradient 间硬切产生闪烁
+ * ============================================================ */
+.dh-page::before,
+.dh-page::after {
+  content: '';
+  position: absolute;
+  inset: 0;
+  z-index: -1;
+  pointer-events: none;
+  transition: opacity 0.5s ease;
+}
+/* 夜间光晕(默认显示) */
+.dh-page::before {
+  background:
+    radial-gradient(1200px 600px at 0% -10%, rgba(124, 58, 237, 0.18), transparent 60%),
+    radial-gradient(900px 600px at 100% 0%, rgba(236, 72, 153, 0.12), transparent 55%);
+  opacity: 1;
+}
+/* 日间光晕(默认隐藏) */
+.dh-page::after {
+  background:
+    radial-gradient(1200px 600px at 0% -10%, rgba(59, 130, 246, 0.1), transparent 60%),
+    radial-gradient(900px 600px at 100% 0%, rgba(6, 182, 212, 0.08), transparent 55%);
+  opacity: 0;
+}
+.app-container.theme-day .dh-page::before { opacity: 0; }
+.app-container.theme-day .dh-page::after { opacity: 1; }
+
+/* ===== 数字人合成页:参考视频模块 ===== */
+.dh-ref-card { padding: 18px 22px; }
+.dh-ref-card__head {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 14px;
+}
+.dh-ref-card__title {
+  font-size: 15px;
+  font-weight: 700;
+  color: var(--dh-text);
+  margin: 0 0 4px;
+}
+.dh-ref-card__desc {
+  font-size: 12px;
+  line-height: 1.55;
+  color: var(--dh-text-3);
+  margin: 0;
+}
+.dh-ref-card__body { display: flex; flex-direction: column; gap: 12px; }
+.dh-ref-drop { padding: 22px 16px; }
+.dh-ref-preview {
+  display: flex;
+  gap: 14px;
+  align-items: flex-start;
+  background: rgba(15, 23, 42, 0.35);
+  border: 1px solid var(--dh-border);
+  border-radius: 12px;
+  padding: 12px;
+}
+.dh-ref-preview__video {
+  width: 200px;
+  max-width: 36%;
+  border-radius: 8px;
+  background: #000;
+  flex-shrink: 0;
+}
+.dh-ref-preview__meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
+.dh-ref-preview__name {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--dh-text);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.dh-ref-preview__sub { font-size: 12px; color: var(--dh-text-3); }
+.dh-ref-preview__actions { margin-top: auto; display: flex; gap: 8px; }
+.dh-ref-progress {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 12px;
+  color: var(--dh-text-2);
+  padding: 8px 12px;
+  background: rgba(168, 85, 247, 0.08);
+  border: 1px solid rgba(168, 85, 247, 0.18);
+  border-radius: 10px;
+}
+.dh-ref-result { display: flex; flex-direction: column; gap: 12px; }
+.dh-ref-analysis {
+  font-size: 12px;
+  line-height: 1.6;
+  color: var(--dh-text-2);
+  padding: 10px 12px;
+  background: rgba(15, 23, 42, 0.35);
+  border: 1px solid var(--dh-border);
+  border-radius: 8px;
+  white-space: pre-wrap;
+  max-height: 160px;
+  overflow-y: auto;
+}
+.dh-ref-rewrite-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+.dh-ref-rewrite-hint { font-size: 11px; color: var(--dh-text-3); }
+.dh-cta__spinner-row { display: inline-flex; align-items: center; gap: 6px; }
+@media (max-width: 720px) {
+  .dh-ref-preview { flex-direction: column; }
+  .dh-ref-preview__video { width: 100%; max-width: 100%; }
+}
+
+/* 日间主题适配 */
+.app-container.theme-day .dh-ref-preview { background: rgba(15, 23, 42, 0.04); }
+.app-container.theme-day .dh-ref-analysis { background: rgba(15, 23, 42, 0.04); }
+.app-container.theme-day .dh-ref-progress { background: rgba(0, 113, 227, 0.06); border-color: rgba(0, 113, 227, 0.18); }
+
+/* ===== 数字人合成页:AI 对话助手 ===== */
+.dh-chat-card { padding: 18px 22px; }
+.dh-chat-card__head {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 14px;
+}
+.dh-chat-card__title {
+  font-size: 15px;
+  font-weight: 700;
+  color: var(--dh-text);
+  margin: 0 0 4px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.dh-chat-card__title::before {
+  content: '';
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--dh-grad);
+  box-shadow: 0 0 12px rgba(168, 85, 247, 0.6);
+}
+.dh-chat-card__desc {
+  font-size: 12px;
+  line-height: 1.55;
+  color: var(--dh-text-3);
+  margin: 0;
+}
+
+.dh-chat-quick {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 14px;
+}
+.dh-chat-quick__chip {
+  padding: 6px 12px;
+  border-radius: 999px;
+  border: 1px solid var(--dh-border);
+  background: rgba(255, 255, 255, 0.04);
+  color: var(--dh-text-2);
+  font-size: 12px;
+  cursor: pointer;
+  transition: all 0.15s ease;
+}
+.dh-chat-quick__chip:hover:not(:disabled) {
+  border-color: rgba(168, 85, 247, 0.5);
+  background: rgba(168, 85, 247, 0.12);
+  color: var(--dh-text);
+  transform: translateY(-1px);
+}
+.dh-chat-quick__chip:disabled { opacity: 0.45; cursor: not-allowed; }
+
+.dh-chat-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  max-height: 320px;
+  overflow-y: auto;
+  overscroll-behavior: contain;
+  padding: 14px;
+  margin-bottom: 12px;
+  border: 1px solid var(--dh-border);
+  border-radius: 12px;
+  background: rgba(0, 0, 0, 0.16);
+  scrollbar-width: thin;
+  scrollbar-color: rgba(168, 85, 247, 0.5) transparent;
+}
+.dh-chat-list::-webkit-scrollbar { width: 8px; }
+.dh-chat-list::-webkit-scrollbar-track { background: transparent; }
+.dh-chat-list::-webkit-scrollbar-thumb {
+  background: rgba(168, 85, 247, 0.35);
+  border-radius: 8px;
+  border: 2px solid transparent;
+  background-clip: padding-box;
+}
+.dh-chat-list::-webkit-scrollbar-thumb:hover { background: rgba(168, 85, 247, 0.6); background-clip: padding-box; }
+.dh-chat-textarea {
+  max-height: 120px;
+  overflow-y: auto;
+}
+.dh-chat-msg { display: flex; gap: 10px; }
+.dh-chat-msg--user { justify-content: flex-end; }
+.dh-chat-msg--ai { justify-content: flex-start; }
+
+.dh-chat-avatar {
+  flex-shrink: 0;
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  background: var(--dh-grad);
+  color: #fff;
+  font-size: 11px;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4px 12px rgba(168, 85, 247, 0.35);
+}
+
+.dh-chat-bubble {
+  max-width: 78%;
+  padding: 10px 14px;
+  border-radius: 14px;
+  font-size: 13.5px;
+  line-height: 1.55;
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+.dh-chat-bubble--user {
+  background: var(--dh-grad);
+  color: #fff;
+  border-bottom-right-radius: 4px;
+  box-shadow: 0 6px 18px rgba(168, 85, 247, 0.32);
+}
+.dh-chat-bubble--ai {
+  background: rgba(255, 255, 255, 0.06);
+  color: var(--dh-text);
+  border: 1px solid var(--dh-border);
+  border-bottom-left-radius: 4px;
+}
+.dh-chat-bubble__text { margin-bottom: 4px; }
+
+.dh-chat-suggest {
+  margin-top: 10px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  background: rgba(0, 0, 0, 0.22);
+  border: 1px dashed var(--dh-border-2);
+}
+.dh-chat-suggest__label {
+  font-size: 11px;
+  color: var(--dh-text-3);
+  font-weight: 600;
+  letter-spacing: 0.04em;
+  text-transform: uppercase;
+  margin-bottom: 4px;
+}
+.dh-chat-suggest__text {
+  font-size: 13px;
+  color: var(--dh-text);
+  line-height: 1.55;
+  white-space: pre-wrap;
+}
+
+.dh-chat-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 10px;
+}
+.dh-chat-apply {
+  padding: 6px 12px;
+  border-radius: 8px;
+  border: 1px solid var(--dh-border-2);
+  background: rgba(255, 255, 255, 0.06);
+  color: var(--dh-text);
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.15s ease;
+}
+.dh-chat-apply:hover:not(:disabled) {
+  background: rgba(168, 85, 247, 0.18);
+  border-color: rgba(168, 85, 247, 0.5);
+}
+.dh-chat-apply:disabled { opacity: 0.55; cursor: not-allowed; }
+.dh-chat-apply--primary {
+  background: var(--dh-grad);
+  border-color: transparent;
+  color: #fff;
+  box-shadow: 0 4px 12px rgba(168, 85, 247, 0.35);
+}
+.dh-chat-apply--primary:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); }
+
+.dh-chat-inputbar {
+  display: flex;
+  align-items: flex-end;
+  gap: 10px;
+}
+.dh-chat-textarea { flex: 1; resize: vertical; min-height: 56px; }
+.dh-chat-send {
+  width: auto !important;
+  min-width: 96px;
+  margin-top: 0 !important;
+  padding: 10px 18px !important;
+}
+
+/* 日间主题适配 */
+.app-container.theme-day .dh-chat-list { background: rgba(15, 23, 42, 0.04); }
+.app-container.theme-day .dh-chat-bubble--ai { background: rgba(15, 23, 42, 0.04); }
+.app-container.theme-day .dh-chat-suggest { background: rgba(15, 23, 42, 0.05); }
+.app-container.theme-day .dh-chat-quick__chip { background: rgba(15, 23, 42, 0.04); }
+
+@media (max-width: 720px) {
+  .dh-chat-bubble { max-width: 88%; }
+  .dh-chat-inputbar { flex-direction: column; align-items: stretch; }
+  .dh-chat-send { width: 100% !important; }
+}
+
+/* ============================================================ */
+/*  数字人合成页:4 栏 Step 流程板                                */
+/* ============================================================ */
+.dh-board {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(0, 1fr));
+  gap: 16px;
+  margin-bottom: 24px;
+}
+@media (max-width: 1280px) {
+  .dh-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+@media (max-width: 720px) {
+  .dh-board { grid-template-columns: minmax(0, 1fr); }
+}
+
+.dh-step-col {
+  background: var(--dh-card);
+  border: 1px solid var(--dh-border);
+  border-radius: 16px;
+  backdrop-filter: blur(14px);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 480px;
+}
+.dh-step-col__head {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 14px 18px 12px;
+  border-bottom: 1px solid var(--dh-border);
+  position: relative;
+}
+.dh-step-col__head::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 14px;
+  bottom: 12px;
+  width: 3px;
+  border-radius: 0 3px 3px 0;
+  background: var(--dh-grad);
+}
+.dh-step-col__title {
+  margin: 0;
+  font-size: 14.5px;
+  font-weight: 700;
+  color: var(--dh-text);
+  flex: 1;
+}
+.dh-step-col__step {
+  font-size: 11px;
+  font-weight: 600;
+  color: var(--dh-text-3);
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  margin-left: 6px;
+  order: 2;
+}
+.dh-step-col__title { order: 1; }
+.dh-step-col__action {
+  order: 3;
+  margin-left: auto;
+}
+.dh-step-col__counter {
+  font-size: 11px;
+  color: var(--dh-text-3);
+  background: rgba(168, 85, 247, 0.12);
+  border: 1px solid rgba(168, 85, 247, 0.3);
+  border-radius: 6px;
+  padding: 2px 8px;
+}
+.dh-step-col__body {
+  padding: 16px 18px 18px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  flex: 1;
+}
+/* Step 2 内容栏的 textarea 限高,避免独占整列 */
+.dh-step-col textarea.dh-textarea {
+  max-height: 160px;
+  resize: vertical;
+}
+
+/* Step 2: 补充提示词预设 chips */
+.dh-preset-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+  margin-top: 8px;
+}
+.dh-preset-chip {
+  font-size: 11.5px;
+  padding: 5px 10px;
+  border-radius: 999px;
+  border: 1px solid var(--dh-border);
+  background: rgba(255, 255, 255, 0.04);
+  color: var(--dh-text-2);
+  cursor: pointer;
+  transition: all 0.15s ease;
+  white-space: nowrap;
+}
+.dh-preset-chip:hover {
+  border-color: rgba(168, 85, 247, 0.5);
+  background: rgba(168, 85, 247, 0.1);
+  color: var(--dh-text);
+}
+.dh-preset-chip.is-active {
+  border-color: rgba(168, 85, 247, 0.7);
+  background: rgba(168, 85, 247, 0.22);
+  color: #fff;
+  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.4) inset;
+}
+.app-container.theme-day .dh-preset-chip {
+  background: rgba(15, 23, 42, 0.04);
+  color: #475569;
+}
+.app-container.theme-day .dh-preset-chip.is-active {
+  background: rgba(168, 85, 247, 0.18);
+  color: #6d28d9;
+}
+
+/* "已同步到口播" ghost chip */
+.dh-chip-btn--ghost {
+  background: rgba(52, 211, 153, 0.14) !important;
+  border-color: rgba(52, 211, 153, 0.4) !important;
+  color: #34d399 !important;
+}
+.dh-step-col__stack { display: flex; flex-direction: column; gap: 12px; }
+.dh-step-col__hint { font-size: 11.5px; color: var(--dh-text-3); margin-top: 10px; line-height: 1.55; }
+
+/* 子模块 (在一栏中分隔多个块) */
+.dh-substep {
+  border: 1px solid var(--dh-border);
+  border-radius: 12px;
+  background: rgba(255,255,255,0.02);
+  padding: 12px 14px;
+}
+.dh-substep__head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  margin-bottom: 10px;
+}
+.dh-substep__head h4 {
+  margin: 0;
+  font-size: 12.5px;
+  font-weight: 600;
+  color: var(--dh-text);
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+.dh-substep__head h4 em { color: var(--dh-error); font-style: normal; font-weight: 500; }
+
+/* Step 1: 参考视频 预览卡 */
+.dh-ref-preview-card {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  padding: 10px;
+  border-radius: 12px;
+  background: rgba(0,0,0,0.18);
+  border: 1px solid var(--dh-border);
+}
+.dh-ref-preview-card__video {
+  position: relative;
+  border-radius: 8px;
+  overflow: hidden;
+  background: #000;
+  aspect-ratio: 16 / 9;
+}
+.dh-ref-preview-card__video video {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+}
+.dh-ref-preview-card__name {
+  font-size: 12.5px;
+  color: var(--dh-text);
+  font-weight: 500;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.dh-ref-preview-card__size { font-size: 11px; color: var(--dh-text-3); }
+
+/* Step 3: 形象图卡 */
+.dh-image-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 18px 14px;
+  border: 1px dashed var(--dh-border-2);
+  border-radius: 12px;
+  background: rgba(255,255,255,0.03);
+  cursor: pointer;
+  transition: all 0.15s ease;
+  color: var(--dh-text-2);
+  text-align: center;
+}
+.dh-image-card:hover {
+  border-color: rgba(168, 85, 247, 0.5);
+  background: rgba(168, 85, 247, 0.06);
+  color: var(--dh-text);
+}
+.dh-image-card__title { font-size: 13px; font-weight: 600; color: var(--dh-text); }
+.dh-image-card__hints {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  font-size: 11px;
+  color: var(--dh-text-3);
+  line-height: 1.7;
+}
+.dh-image-card__hints li::before { content: '• '; color: var(--dh-text-3); }
+.dh-image-card--filled {
+  flex-direction: row;
+  align-items: flex-start;
+  cursor: default;
+  border-style: solid;
+  padding: 12px;
+  text-align: left;
+  gap: 12px;
+}
+.dh-image-card--filled:hover { background: rgba(255,255,255,0.03); border-color: var(--dh-border-2); color: var(--dh-text-2); }
+.dh-image-card--filled img {
+  width: 72px;
+  height: 72px;
+  object-fit: cover;
+  border-radius: 10px;
+  flex-shrink: 0;
+}
+.dh-image-card__meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
+.dh-image-card__name {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--dh-text);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.dh-image-card__size { font-size: 11px; color: var(--dh-text-3); }
+.dh-image-card__actions { display: flex; gap: 6px; margin-top: 4px; }
+
+/* Step 3: 音色卡列表 */
+.dh-voice-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  margin-top: 10px;
+}
+.dh-voice-card {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  border: 1px solid var(--dh-border);
+  background: rgba(255,255,255,0.03);
+  color: var(--dh-text-2);
+  cursor: pointer;
+  transition: all 0.15s ease;
+  font-size: 12.5px;
+  text-align: left;
+  width: 100%;
+}
+.dh-voice-card:hover {
+  border-color: rgba(168, 85, 247, 0.4);
+  background: rgba(168, 85, 247, 0.08);
+  color: var(--dh-text);
+}
+.dh-voice-card.is-active {
+  border-color: rgba(168, 85, 247, 0.6);
+  background: rgba(168, 85, 247, 0.16);
+  color: var(--dh-text);
+  box-shadow: 0 0 0 1px rgba(168, 85, 247, 0.4) inset;
+}
+.dh-voice-card__dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--dh-grad);
+  flex-shrink: 0;
+}
+.dh-voice-card__name { flex: 1; font-weight: 500; }
+.dh-voice-card__check {
+  font-size: 13px;
+  color: var(--dh-text-3);
+  opacity: 0;
+}
+.dh-voice-card.is-active .dh-voice-card__check { opacity: 1; color: #a855f7; }
+
+/* Step 4: 预览主区 */
+.dh-preview-stage {
+  border-radius: 12px;
+  border: 1px solid var(--dh-border);
+  background: rgba(0,0,0,0.22);
+  min-height: 220px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  padding: 20px;
+  text-align: center;
+}
+.dh-preview-stage__icon {
+  width: 56px;
+  height: 56px;
+  border-radius: 50%;
+  background: rgba(168, 85, 247, 0.18);
+  border: 1px solid rgba(168, 85, 247, 0.4);
+  color: #c084fc;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-bottom: 6px;
+}
+.dh-preview-stage__title { margin: 0; font-size: 13.5px; font-weight: 600; color: var(--dh-text); }
+.dh-preview-stage__sub { margin: 0; font-size: 12px; color: var(--dh-text-3); }
+.dh-preview-stage__loading { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
+.dh-preview-stage__loading .dh-progress-bar { width: 100%; max-width: 240px; }
+.dh-preview-stage__done { width: 100%; padding: 0; }
+.dh-preview-stage__done video {
+  width: 100%;
+  max-height: 320px;
+  border-radius: 8px;
+  background: #000;
+  display: block;
+}
+.dh-preview-stage__cross {
+  width: 40px; height: 40px; border-radius: 50%;
+  background: rgba(248, 113, 113, 0.18); color: var(--dh-error);
+  display: inline-flex; align-items: center; justify-content: center;
+  font-weight: 700; font-size: 18px;
+}
+.dh-spinner--lg { width: 32px; height: 32px; border-width: 3px; }
+
+/* Step 4: 大 CTA */
+.dh-cta--xl {
+  margin-top: 4px;
+  font-size: 14px;
+  padding: 14px 20px;
+}
+.dh-cta--sm {
+  width: auto;
+  font-size: 12.5px;
+  padding: 8px 16px;
+  margin-top: 0;
+}
+
+/* Step 4: 状态清单 */
+.dh-checklist {
+  list-style: none;
+  margin: 4px 0 0;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+.dh-checklist__item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 10px;
+  border-radius: 8px;
+  background: rgba(255,255,255,0.02);
+  border: 1px solid var(--dh-border);
+  font-size: 12px;
+  color: var(--dh-text-2);
+}
+.dh-checklist__dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.18);
+  flex-shrink: 0;
+}
+.dh-checklist__label { flex: 1; }
+.dh-checklist__status { font-size: 11px; color: var(--dh-text-3); }
+.dh-checklist__item.is-done { color: var(--dh-text); }
+.dh-checklist__item.is-done .dh-checklist__dot { background: var(--dh-success, #34d399); box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.18); }
+.dh-checklist__item.is-done .dh-checklist__status { color: var(--dh-success, #34d399); }
+.dh-checklist__item.is-active .dh-checklist__dot {
+  background: #a855f7;
+  box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.18);
+  animation: dh-pulse 1.4s ease-in-out infinite;
+}
+.dh-checklist__item.is-active .dh-checklist__status { color: #c084fc; }
+@keyframes dh-pulse {
+  0%, 100% { transform: scale(1); }
+  50% { transform: scale(1.25); }
+}
+
+/* Step 4: 完成后的操作栏 */
+.dh-preview-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+.dh-preview-action {
+  flex: 1;
+  min-width: 100px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 9px 12px;
+  border-radius: 10px;
+  border: 1px solid var(--dh-border);
+  background: rgba(255,255,255,0.04);
+  color: var(--dh-text);
+  font-size: 12px;
+  font-weight: 500;
+  cursor: pointer;
+  text-decoration: none;
+  transition: all 0.15s ease;
+}
+.dh-preview-action:hover {
+  background: rgba(168, 85, 247, 0.12);
+  border-color: rgba(168, 85, 247, 0.4);
+}
+
+/* Step 2: AI 风格助手折叠面板 */
+.dh-chat-fold { padding: 0; overflow: hidden; }
+.dh-chat-fold__summary {
+  padding: 12px 14px;
+  cursor: pointer;
+  list-style: none;
+  margin: 0 !important;
+}
+.dh-chat-fold__summary::-webkit-details-marker { display: none; }
+.dh-chat-fold[open] .dh-chat-fold__summary { border-bottom: 1px solid var(--dh-border); margin-bottom: 0 !important; }
+.dh-chat-fold__summary h4 { display: flex; align-items: center; gap: 8px; }
+.dh-chat-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--dh-grad);
+  box-shadow: 0 0 8px rgba(168, 85, 247, 0.5);
+}
+.dh-chat-fold__badge {
+  font-size: 10px;
+  background: rgba(168, 85, 247, 0.18);
+  color: #c084fc;
+  border-radius: 8px;
+  padding: 1px 6px;
+  font-weight: 600;
+}
+.dh-chat-fold__desc { padding: 12px 14px 0; margin: 0 0 10px; font-size: 11.5px; color: var(--dh-text-3); line-height: 1.55; }
+.dh-chat-fold .dh-chat-quick,
+.dh-chat-fold .dh-chat-list,
+.dh-chat-fold .dh-chat-inputbar,
+.dh-chat-fold > .dh-error { margin-left: 14px; margin-right: 14px; }
+.dh-chat-fold .dh-chat-inputbar { padding-bottom: 14px; }
+.dh-chat-fold .dh-chat-list { max-height: 280px; }
+
+/* 兼容旧选择器(其他页面仍可能使用 .dh-card) */
+.dh-step-col .dh-textarea,
+.dh-step-col .dh-input { width: 100%; box-sizing: border-box; }
+
+/* 日间主题适配 */
+.app-container.theme-day .dh-step-col {
+  background: rgba(255, 255, 255, 0.85);
+  border-color: rgba(37, 99, 235, 0.14);
+}
+.app-container.theme-day .dh-substep {
+  background: rgba(255, 255, 255, 0.92);
+  border-color: rgba(37, 99, 235, 0.14);
+}
+.app-container.theme-day .dh-substep__head h4 { color: #0f172a; }
+.app-container.theme-day .dh-ref-preview-card { background: rgba(15, 23, 42, 0.04); }
+.app-container.theme-day .dh-preview-stage { background: rgba(15, 23, 42, 0.04); }
+.app-container.theme-day .dh-image-card {
+  background: rgba(15, 23, 42, 0.03);
+  border-color: rgba(37, 99, 235, 0.25);
+  color: #475569;
+}
+.app-container.theme-day .dh-image-card:hover {
+  background: rgba(37, 99, 235, 0.06);
+  border-color: rgba(37, 99, 235, 0.5);
+  color: #0f172a;
+}
+.app-container.theme-day .dh-image-card__title,
+.app-container.theme-day .dh-image-card__name { color: #0f172a; }
+.app-container.theme-day .dh-image-card__hints,
+.app-container.theme-day .dh-image-card__size { color: #64748b; }
+.app-container.theme-day .dh-voice-card {
+  background: rgba(15, 23, 42, 0.03);
+  border-color: rgba(37, 99, 235, 0.18);
+  color: #475569;
+}
+.app-container.theme-day .dh-voice-card.is-active {
+  background: rgba(37, 99, 235, 0.1);
+  border-color: rgba(37, 99, 235, 0.5);
+  color: #0f172a;
+}
+.app-container.theme-day .dh-checklist__item {
+  background: rgba(15, 23, 42, 0.03);
+  border-color: rgba(37, 99, 235, 0.14);
+  color: #475569;
+}
+.app-container.theme-day .dh-preview-action {
+  background: rgba(15, 23, 42, 0.04);
+  border-color: rgba(37, 99, 235, 0.18);
+  color: #0f172a;
+}
+
+/* ★ 日间主题:segmented control 适配(修复音频来源 / 720P-1080P 颜色突兀) */
+.app-container.theme-day .dh-segment {
+  background: rgba(15, 23, 42, 0.05);
+  border-color: rgba(37, 99, 235, 0.18);
+}
+.app-container.theme-day .dh-segment__item {
+  color: #475569;
+}
+.app-container.theme-day .dh-segment__item:hover { color: #0f172a; }
+.app-container.theme-day .dh-segment__item.is-active {
+  /* 保持紫色渐变高亮,但日间用蓝色更协调 */
+  background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
+  color: #fff;
+  box-shadow: 0 6px 18px rgba(37, 99, 235, 0.25);
+}
+
+/* ★ 日间主题:toggle 适配(修复"快速模式"开关白底白色看不见) */
+.app-container.theme-day .dh-toggle__track {
+  background: rgba(15, 23, 42, 0.18);
+}
+.app-container.theme-day .dh-toggle__thumb {
+  background: #fff;
+  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
+}
+.app-container.theme-day .dh-toggle input:checked + .dh-toggle__track {
+  background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
+}
+.app-container.theme-day .dh-toggle__label { color: #0f172a; }
+.app-container.theme-day .dh-toggle__label small { color: #64748b; }
+
+/* ★ 日间主题:dh-chip-btn(更换 / 移除)适配 */
+.app-container.theme-day .dh-chip-btn {
+  background: rgba(15, 23, 42, 0.05);
+  border-color: rgba(37, 99, 235, 0.2);
+  color: #334155;
+}
+.app-container.theme-day .dh-chip-btn:hover {
+  background: rgba(37, 99, 235, 0.08);
+  color: #0f172a;
+}
+
+/* ★ 日间主题:dh-field 标签 / 计数器 / 错误星号 */
+.app-container.theme-day .dh-field__label > span:first-child { color: #0f172a; }
+.app-container.theme-day .dh-field__counter { color: #94a3b8; }
+.app-container.theme-day .dh-substep__head h4 em { color: #dc2626; }
+
+/* ===== AI重塑模式选择弹窗 ===== */
+.remix-mode-chooser-overlay {
+  background: rgba(15, 23, 42, 0.55) !important;
+  backdrop-filter: blur(6px);
+}
+.remix-mode-chooser {
+  width: 92%;
+  max-width: 520px;
+  background: #ffffff;
+  color: #0f172a;
+  border-radius: 16px;
+  box-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);
+  border: 1px solid rgba(15, 23, 42, 0.08);
+  padding: 22px 24px 24px;
+  animation: slideUp 0.2s ease;
+}
+.remix-mode-chooser__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+.remix-mode-chooser__header h3 {
+  margin: 0;
+  font-size: 17px;
+  font-weight: 600;
+  color: #0f172a;
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+}
+.remix-mode-chooser__header h3 .icon {
+  color: #0071e3;
+}
+.remix-mode-chooser__close {
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  border: none;
+  background: rgba(15, 23, 42, 0.06);
+  color: #475569;
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  transition: background 0.15s ease;
+}
+.remix-mode-chooser__close:hover {
+  background: rgba(15, 23, 42, 0.12);
+  color: #0f172a;
+}
+.remix-mode-chooser__subtitle {
+  margin: 6px 0 16px;
+  font-size: 12px;
+  color: #64748b;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.remix-mode-chooser__grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+}
+.remix-mode-chooser__card {
+  text-align: left;
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+  border-radius: 12px;
+  padding: 16px;
+  cursor: pointer;
+  transition: all 0.18s ease;
+  font: inherit;
+  color: inherit;
+}
+.remix-mode-chooser__card:hover {
+  background: #f1f5ff;
+  border-color: #0071e3;
+  box-shadow: 0 6px 16px rgba(0, 113, 227, 0.12);
+  transform: translateY(-1px);
+}
+.remix-mode-chooser__card-title {
+  font-size: 15px;
+  font-weight: 600;
+  color: #0f172a;
+  margin-bottom: 6px;
+}
+.remix-mode-chooser__card-desc {
+  font-size: 12px;
+  line-height: 1.55;
+  color: #64748b;
+}
+@media (max-width: 520px) {
+  .remix-mode-chooser__grid { grid-template-columns: 1fr; }
+}
+
+/* ============================================================
+ * UI/UX Pro Max — Design Polish Layer
+ * Source: docs/ui-ux-pro-max-skill-main
+ * Targets: SaaS dashboard / Short Video Editor
+ * Principles applied:
+ *   - Inter typography w/ tabular-nums for data
+ *   - Modern SaaS scale: H1 32pt / H2 24pt / Body 16pt / Label 12pt(+1.2 tracking)
+ *   - Glassmorphism + Flat Design micro-interactions (150-200ms ease)
+ *   - Radius system 6 / 12 / 999, 44px+ touch targets
+ *   - Active nav left-bar indicator for wayfinding
+ *   - Consistent focus ring & hover lift
+ * ============================================================ */
+
+/* ---------- Typography rhythm ---------- */
+.app-container {
+  font-feature-settings: 'cv11', 'ss01', 'ss03';
+}
+
+/* Tone down oversized hero H2s (40px) into modern SaaS H1 scale (32px). */
+.app-container .search-header h2,
+.app-container .monitor-header h2,
+.app-container .tasks-header h2,
+.app-container .history-header h2,
+.app-container .results-header h2,
+.app-container .videos-header h2 {
+  font-size: 32px;
+  font-weight: 700;
+  letter-spacing: -0.022em;
+  line-height: 1.15;
+}
+
+.app-container .search-header .subtitle,
+.app-container .monitor-header .subtitle {
+  font-size: 15px;
+  letter-spacing: -0.005em;
+  line-height: 1.55;
+}
+
+/* Section/secondary H3 modernization */
+.app-container .results-header h3,
+.app-container .active-tasks h3,
+.app-container .monitor-work-header h3,
+.app-container .voice-panel-head h3 {
+  font-size: 20px;
+  font-weight: 600;
+  letter-spacing: -0.012em;
+}
+
+/* Small labels — uppercase tracked per skill (Inter 500 +1.2 tracking) */
+.app-container .form-field label,
+.app-container .monitor-author-stat-label,
+.app-container .overview-label,
+.app-container .stat-label,
+.app-container .preview-info-label,
+.app-container .monitor-summary-label {
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  font-size: 11px;
+  font-weight: 600;
+}
+
+/* Numeric values — tabular figures for data-dense readability */
+.app-container .monitor-author-stat-value,
+.app-container .overview-number,
+.app-container .stat-value,
+.app-container .interaction-stat,
+.app-container .monitor-summary-card strong,
+.app-container .video-id,
+.app-container .history-id,
+.app-container .task-id {
+  font-variant-numeric: tabular-nums;
+  font-feature-settings: 'tnum' 1, 'cv11' 1;
+}
+
+/* Monospace surfaces should use JetBrains Mono per skill */
+.app-container .task-id,
+.app-container .history-id,
+.app-container code,
+.app-container pre {
+  font-family: 'JetBrains Mono', 'Monaco', 'Menlo', 'Cascadia Mono', monospace;
+  font-size: 12px;
+  letter-spacing: 0;
+}
+
+/* ---------- Buttons: 44px touch targets + flat-design hover ---------- */
+.app-container .btn {
+  min-height: 44px;
+  font-weight: 500;
+  letter-spacing: -0.005em;
+  transition:
+    background-color 160ms ease,
+    color 160ms ease,
+    border-color 160ms ease,
+    transform 120ms ease,
+    box-shadow 160ms ease;
+}
+.app-container .btn:active:not(:disabled) {
+  transform: translateY(0) scale(0.98);
+  transition-duration: 80ms;
+}
+
+/* Stronger, clearer focus ring (WCAG AA) */
+.app-container .btn:focus-visible,
+.app-container .icon-btn:focus-visible,
+.app-container .form-field input:focus-visible,
+.app-container .form-field select:focus-visible,
+.app-container .form-field textarea:focus-visible,
+.app-container .nav-item:focus-visible {
+  outline: none;
+  box-shadow:
+    0 0 0 2px var(--background-color),
+    0 0 0 4px var(--primary-color);
+}
+
+/* ---------- Sidebar nav: left-bar active indicator ---------- */
+.app-container .nav-item {
+  position: relative;
+}
+.app-container .nav-item::before {
+  content: '';
+  position: absolute;
+  left: -16px;
+  top: 50%;
+  transform: translateY(-50%) scaleY(0);
+  width: 3px;
+  height: 22px;
+  border-radius: 0 3px 3px 0;
+  background: var(--primary-color);
+  transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 180ms ease;
+  opacity: 0;
+}
+.app-container .nav-item.active::before {
+  transform: translateY(-50%) scaleY(1);
+  opacity: 1;
+}
+
+/* ---------- Cards: unified hover lift & radius (12px) ---------- */
+.app-container .video-card,
+.app-container .result-card,
+.app-container .video-item,
+.app-container .monitor-author-card,
+.app-container .monitor-work-card,
+.app-container .history-card,
+.app-container .task-card,
+.app-container .voice-card,
+.app-container .monitor-summary-card,
+.app-container .overview-card {
+  border-radius: 14px;
+  transition:
+    transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1),
+    box-shadow 180ms ease,
+    border-color 180ms ease;
+}
+.app-container .video-card:hover,
+.app-container .result-card:hover,
+.app-container .video-item:hover,
+.app-container .monitor-author-card:hover,
+.app-container .monitor-work-card:hover,
+.app-container .history-card:hover {
+  transform: translateY(-3px);
+}
+
+/* ---------- Status badges: pill radius + tighter typo ---------- */
+.app-container .status-badge {
+  border-radius: 999px;
+  letter-spacing: 0.06em;
+  font-size: 11px;
+  padding: 4px 10px;
+}
+
+/* ---------- Tag chips: subtle pill with hover affordance ---------- */
+.app-container .tag-list .tag,
+.app-container .video-tags .tag,
+.app-container .video-meta-chip,
+.app-container .voice-meta-chip {
+  border-radius: 999px;
+  font-weight: 500;
+  letter-spacing: -0.005em;
+  transition: background-color 150ms ease, color 150ms ease;
+}
+
+/* ---------- Workbench: subtle data-dense gradient backdrop ---------- */
+.app-container.theme-day .workbench {
+  background:
+    radial-gradient(1200px 600px at 0% -10%, rgba(59, 130, 246, 0.06), transparent 60%),
+    radial-gradient(900px 500px at 100% 0%, rgba(6, 182, 212, 0.05), transparent 60%),
+    var(--background-color);
+}
+
+/* ---------- Empty states: more refined ---------- */
+.app-container .empty-state {
+  border-radius: 16px;
+  border: 1px dashed rgba(0, 113, 227, 0.22);
+  background:
+    linear-gradient(180deg, rgba(255,255,255,0.6), rgba(255,255,255,0.3));
+}
+.app-container .empty-state h3 {
+  font-size: 18px;
+  font-weight: 600;
+  letter-spacing: -0.012em;
+}
+.app-container .empty-state p {
+  font-size: 14px;
+  line-height: 1.6;
+}
+
+/* ---------- Forms: 44px height + smoother focus ---------- */
+.app-container .form-field input,
+.app-container .form-field select,
+.app-container .form-field textarea {
+  min-height: 44px;
+  border-radius: 12px;
+  transition:
+    border-color 150ms ease,
+    box-shadow 150ms ease,
+    background-color 150ms ease;
+}
+
+/* ---------- Sidebar nav: tighter typography ---------- */
+.app-container .nav-text {
+  font-size: 14px;
+  font-weight: 500;
+  letter-spacing: -0.005em;
+}
+
+/* ---------- Header logo: refined wordmark ---------- */
+.app-container .header .logo {
+  font-weight: 600;
+  letter-spacing: -0.018em;
+}
+.app-container .header .logo small {
+  margin-left: 6px;
+  padding: 2px 8px;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.12);
+  font-size: 10px;
+  font-weight: 500;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+}
+.app-container.theme-day .header .logo small {
+  background: rgba(37, 99, 235, 0.1) !important;
+  color: rgba(30, 58, 138, 0.9) !important;
+  -webkit-text-fill-color: rgba(30, 58, 138, 0.9);
+}
+
+/* ---------- Reduced motion accessibility ---------- */
+@media (prefers-reduced-motion: reduce) {
+  .app-container *,
+  .app-container *::before,
+  .app-container *::after {
+    transition-duration: 0.01ms !important;
+    animation-duration: 0.01ms !important;
+    animation-iteration-count: 1 !important;
+  }
+}

+ 6450 - 0
src/app/app.html

@@ -0,0 +1,6450 @@
+<div class="app-container" [class.theme-day]="themeMode === 'day'" [class.theme-night]="themeMode === 'night'">
+
+  <!-- 头部导航 -->
+
+  <header class="header shadow">
+
+    <span class="logo">抖音AI视频生成系统 <small>v1.0</small></span>
+
+    <span class="spacer"></span>
+
+    <button class="theme-toggle" type="button" (click)="toggleTheme()"
+
+            [attr.aria-label]="themeMode === 'day' ? '切换到夜间模式' : '切换到日间模式'"
+
+            [title]="themeMode === 'day' ? '切换到夜间模式' : '切换到日间模式'">
+
+      <!-- 日间模式显示月亮(点击切夜间),夜间模式显示太阳(点击切日间) -->
+
+      <svg *ngIf="themeMode === 'day'" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+        <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
+
+      </svg>
+
+      <svg *ngIf="themeMode === 'night'" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+        <circle cx="12" cy="12" r="5"></circle>
+
+        <line x1="12" y1="1" x2="12" y2="3"></line>
+
+        <line x1="12" y1="21" x2="12" y2="23"></line>
+
+        <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
+
+        <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
+
+        <line x1="1" y1="12" x2="3" y2="12"></line>
+
+        <line x1="21" y1="12" x2="23" y2="12"></line>
+
+        <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
+
+        <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
+
+      </svg>
+
+    </button>
+
+  </header>
+
+
+
+  <!-- 主内容区域 -->
+
+  <div class="main-content">
+
+    <!-- 侧边栏导航 -->
+
+    <nav class="sidebar">
+
+      <ul class="nav-list">
+
+        <li class="nav-item" [class.active]="currentTab === 'search'" (click)="setCurrentTab('search')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
+
+          <span class="nav-text">视频发现</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'monitor'" (click)="setCurrentTab('monitor')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
+
+          <span class="nav-text">博主监测</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'tasks'" (click)="setCurrentTab('tasks')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
+
+          <span class="nav-text">任务管理</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'history'" (click)="setCurrentTab('history')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9"></path><polyline points="3 4 3 9 8 9"></polyline><polyline points="12 7 12 12 15 14"></polyline></svg>
+
+          <span class="nav-text">生成历史</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'results'" (click)="setCurrentTab('results')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none"></polygon></svg>
+
+          <span class="nav-text">结果展示</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'videos'" (click)="setCurrentTab('videos')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
+
+          <span class="nav-text">视频管理</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'voice-synthesis'" (click)="setCurrentTab('voice-synthesis')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>
+
+          <span class="nav-text">语音合成</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'digital-human'" (click)="setCurrentTab('digital-human')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
+
+          <span class="nav-text">数字人合成</span>
+
+        </li>
+
+        <li class="nav-item" [class.active]="currentTab === 'video-generation'" (click)="setCurrentTab('video-generation')">
+
+          <svg class="nav-svg" viewBox="0 0 24 24"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg>
+
+          <span class="nav-text">视频生成</span>
+
+        </li>
+
+        <li class="nav-item-divider"></li>
+
+      </ul>
+
+    </nav>
+
+
+
+    <!-- 工作区内容 -->
+
+    <main class="workbench">
+
+      <!-- 视频发现模块 -->
+
+      <section class="dh-page search-page" *ngIf="currentTab === 'search'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <circle cx="11" cy="11" r="8"></circle>
+
+              <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>视频发现</h1>
+
+            <p>搜索抖音视频,获取 AI 创作灵感与素材</p>
+
+          </div>
+
+        </header>
+
+
+
+        <div class="search-controls">
+
+          <div class="search-input-group">
+
+            <div class="form-field search-field">
+
+              <label>输入关键词搜索抖音视频</label>
+
+              <input type="text" placeholder="例如:科技博主、AI新闻、产品评测..."
+
+                     [(ngModel)]="searchKeyword">
+
+            </div>
+
+
+
+            <div class="form-field filter-field">
+
+              <label>排序方式</label>
+
+              <select [(ngModel)]="sortType" (ngModelChange)="onSearchFilterChange()">
+
+                <option value="0">综合排序</option>
+
+                <option value="1">最多点赞</option>
+
+                <option value="2">最新发布</option>
+
+              </select>
+
+            </div>
+
+
+
+            <div class="form-field filter-field">
+
+              <label>发布时间</label>
+
+              <select [(ngModel)]="publishTime" (ngModelChange)="onSearchFilterChange()">
+
+                <option value="0">不限</option>
+
+                <option value="1">一天内</option>
+
+                <option value="7">一周内</option>
+
+                <option value="180">半年内</option>
+
+              </select>
+
+            </div>
+
+
+
+            <div class="form-field filter-field">
+
+              <label>视频时长</label>
+
+              <select [(ngModel)]="filterDuration" (ngModelChange)="onSearchFilterChange()">
+
+                <option value="0">不限</option>
+
+                <option value="0-1">1分钟内</option>
+
+                <option value="1-5">1-5分钟</option>
+
+                <option value="5-10000">5分钟以上</option>
+
+              </select>
+
+            </div>
+
+
+
+            <button class="btn btn-primary search-btn"
+
+                    (click)="searchVideos()" [disabled]="!searchKeyword">
+
+              <span class="icon">search</span> 搜索视频
+
+            </button>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="video-results" *ngIf="searchResults.length > 0">
+
+          <div class="results-header">
+
+            <h3>搜索结果 ({{filteredSearchResults.length}} / {{searchResults.length}})</h3>
+
+            <button class="btn" (click)="loadMore()" *ngIf="hasMore">
+
+              <span class="icon">refresh</span> 加载更多
+
+            </button>
+
+          </div>
+
+
+
+          <div class="video-grid" *ngIf="filteredSearchResults.length > 0">
+
+            <div class="video-card" *ngFor="let video of filteredSearchResults">
+
+              <div class="video-thumbnail">
+
+                <img [src]="video.cover_url" [alt]="video.desc" referrerpolicy="no-referrer" (error)="handleVideoCoverError(video)">
+
+                <div class="video-duration">{{video.duration | timeFormat}}</div>
+
+              </div>
+
+              <div class="card-content">
+
+                <h4 class="video-title">{{video.desc}}</h4>
+
+                <div class="video-info-row">
+
+                  <div class="video-meta-row">
+
+                    <span class="video-meta-chip" *ngIf="video.publishDate">
+
+                      {{video.publishDate | date:'MM-dd HH:mm'}}
+
+                    </span>
+
+                    <span class="video-meta-chip" *ngIf="video.duration">
+
+                      {{video.duration | timeFormat}}
+
+                    </span>
+
+                    <span class="video-author-inline">{{video.author.nickname}}</span>
+
+                    <span class="video-id" *ngIf="video.aweme_id">ID: {{video.aweme_id}}</span>
+
+                  </div>
+
+                  <div class="video-interactions">
+
+                    <span class="interaction-stat like">{{video.statistics.digg_count | numberFormat}}</span>
+
+                    <span class="interaction-stat comment">{{video.statistics.comment_count | numberFormat}}</span>
+
+                    <span class="interaction-stat share">{{video.statistics.share_count | numberFormat}}</span>
+
+                  </div>
+
+                </div>
+
+                <div class="video-tags" *ngIf="video.text_extra">
+
+                  <div class="tag-list">
+
+                    <span class="tag" *ngFor="let tag of video.text_extra">
+
+                      #{{tag}}
+
+                    </span>
+
+                  </div>
+
+                </div>
+
+              </div>
+
+              <div class="card-actions">
+
+                <button class="btn" (click)="viewVideoDetail(video)">
+
+                  <span class="icon">info</span> 查看详情
+
+                </button>
+
+                <button class="btn btn-primary" (click)="downloadVideo(video)"
+
+                        [disabled]="video.isDownloading">
+
+                  <span class="icon">{{ video.isDownloading ? 'hourglass_empty' : 'download' }}</span>
+
+                  {{ video.isDownloading ? '下载中 ' + video.downloadProgress + '%' : '下载视频' }}
+
+                </button>
+
+                <button class="btn btn-accent" (click)="startAIGeneration(video)">
+
+                  <span class="icon">auto_awesome</span> AI重塑
+
+                </button>
+
+              </div>
+
+              @if (video.isDownloading) {
+
+              <div class="download-progress">
+
+                <div class="progress-bar">
+
+                  <div class="progress-fill" [style.width]="(video.downloadProgress || 0) + '%'"></div>
+
+                </div>
+
+              </div>
+
+              }
+
+            </div>
+
+          </div>
+
+
+
+          <div class="empty-state" *ngIf="filteredSearchResults.length === 0 && !isSearching">
+
+            <span class="icon large">filter_alt_off</span>
+
+            <h3>没有符合筛选条件的视频</h3>
+
+            <p>请调整排序、发布时间或视频时长后再试</p>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="empty-state" *ngIf="searchResults.length === 0 && !isSearching">
+
+          <span class="icon large">search</span>
+
+          <h3>开始搜索抖音视频</h3>
+
+          <p>输入关键词发现优质内容,让AI帮您重新创作</p>
+
+        </div>
+
+
+
+        <div class="loading-state" *ngIf="isSearching">
+
+          <div class="spinner"></div>
+
+          <p>正在搜索相关视频...</p>
+
+        </div>
+
+      </section>
+
+
+
+      <section class="dh-page monitor-page monitor-section" *ngIf="currentTab === 'monitor'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>博主监测</h1>
+
+            <p>通过抖音号 unique_id 添加博主,快速查看主页信息与作品分布</p>
+
+          </div>
+
+        </header>
+
+        <div class="monitor-header monitor-toolbar-row">
+
+          <div class="monitor-toolbar">
+
+            <div class="form-field search-field">
+
+              <label>输入抖音号(unique_id)添加博主</label>
+
+              <input type="text" placeholder="例如:TheChief" [(ngModel)]="monitorUniqueIdInput" (keyup.enter)="addMonitorAuthor()">
+
+            </div>
+
+            <button class="btn btn-primary search-btn" (click)="addMonitorAuthor()" [disabled]="!monitorUniqueIdInput.trim() || isAddingMonitorAuthor">
+
+              <span class="icon">{{ isAddingMonitorAuthor ? 'hourglass_empty' : 'person_add' }}</span>
+
+              {{ isAddingMonitorAuthor ? '添加中...' : '添加博主' }}
+
+            </button>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="monitor-error" *ngIf="monitorAddError">{{monitorAddError}}</div>
+
+
+
+        <ng-container *ngIf="!getSelectedMonitorAuthor(); else monitorDetailView">
+
+          <div class="monitor-summary" *ngIf="monitorAuthors.length > 0">
+
+            <div class="monitor-summary-card">
+
+              <span class="monitor-summary-label">已监测博主</span>
+
+              <strong>{{monitorAuthors.length}}</strong>
+
+            </div>
+
+            <div class="monitor-summary-card">
+
+              <span class="monitor-summary-label">累计作品</span>
+
+              <strong>{{getTotalMonitorWorksCount()}}</strong>
+
+            </div>
+
+          </div>
+
+
+
+          <div class="monitor-author-grid" *ngIf="monitorAuthors.length > 0">
+
+            <article class="monitor-author-card" *ngFor="let author of monitorAuthors" (click)="openMonitorAuthor(author)">
+
+              <div class="monitor-author-top">
+
+                <div class="monitor-author-identity">
+
+                  <img [src]="author.avatarUrl" [alt]="author.nickname" class="monitor-author-avatar" referrerpolicy="no-referrer" (error)="handleMonitorAuthorAvatarError(author)">
+
+                  <div>
+
+                    <h3>{{author.nickname}}</h3>
+
+                    <p class="monitor-author-verified" *ngIf="author.verifiedText">{{author.verifiedText}}</p>
+
+                    <p class="monitor-author-uid">抖音号:{{author.uniqueId || author.shortId || author.uid}}</p>
+
+                  </div>
+
+                </div>
+
+                <div class="monitor-author-actions">
+
+                  <button class="btn btn-primary" type="button" (click)="$event.stopPropagation(); openMonitorAuthor(author)">
+
+                    查看主页
+
+                  </button>
+
+                  <button class="btn btn-warn" type="button" (click)="$event.stopPropagation(); removeMonitorAuthor(author)">
+
+                    删除
+
+                  </button>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="monitor-author-stats">
+
+                <span class="monitor-author-stat">
+
+                  <span class="monitor-author-stat-label">获赞</span>
+
+                  <strong class="monitor-author-stat-value">{{author.totalFavorited | numberFormat}}</strong>
+
+                </span>
+
+                <span class="monitor-author-stat">
+
+                  <span class="monitor-author-stat-label">粉丝</span>
+
+                  <strong class="monitor-author-stat-value">{{author.followerCount | numberFormat}}</strong>
+
+                </span>
+
+                <span class="monitor-author-stat">
+
+                  <span class="monitor-author-stat-label">作品</span>
+
+                  <strong class="monitor-author-stat-value">{{author.awemeCount | numberFormat}}</strong>
+
+                </span>
+
+              </div>
+
+
+
+              <p class="monitor-author-signature">{{author.signature || '该用户暂未填写简介'}}</p>
+
+
+
+              <div class="monitor-author-preview" *ngIf="author.works.length > 0">
+
+                <div class="monitor-preview-item" *ngFor="let work of author.works.slice(0, 3)">
+
+                  <img [src]="work.coverUrl" [alt]="work.desc" referrerpolicy="no-referrer" (error)="handleMonitorWorkCoverError(work)">
+
+                </div>
+
+              </div>
+
+
+
+              <div class="monitor-author-foot">
+
+                <span>{{getMonitorIpLocationText(author)}}</span>
+
+                <span *ngIf="author.loadingWorks">作品加载中...</span>
+
+                <span *ngIf="!author.loadingWorks">已载入 {{author.works.length}} 条作品</span>
+
+              </div>
+
+            </article>
+
+          </div>
+
+
+
+          <div class="empty-state" *ngIf="monitorAuthors.length === 0 && !isAddingMonitorAuthor">
+
+            <span class="icon large">monitoring</span>
+
+            <h3>还没有监测中的博主</h3>
+
+            <p>输入抖音号 unique_id 后即可把博主加入监测页,并查看主页作品排布</p>
+
+          </div>
+
+        </ng-container>
+
+
+
+        <ng-template #monitorDetailView>
+
+          <ng-container *ngIf="getSelectedMonitorAuthor() as selectedAuthor">
+
+            <div class="monitor-detail-shell">
+
+              <div class="monitor-detail-topbar">
+
+                <button class="btn" (click)="backToMonitorList()">
+
+                  <span class="icon">arrow_back</span> 返回列表
+
+                </button>
+
+                <div class="monitor-detail-actions">
+
+                  <button class="btn btn-warn" (click)="removeMonitorAuthor(selectedAuthor)">
+
+                    <span class="icon">delete</span> 删除博主
+
+                  </button>
+
+                  <button class="btn" (click)="reloadMonitorAuthorWorks(selectedAuthor)">
+
+                    <span class="icon">refresh</span> 刷新作品
+
+                  </button>
+
+                  <button class="btn" (click)="setCurrentTab('search')">
+
+                    <span class="icon">search</span> 去发现页
+
+                  </button>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="monitor-hero" [style.background]="getMonitorHeroBackground(selectedAuthor)">
+
+                <div class="monitor-hero-main">
+
+                  <img [src]="selectedAuthor.avatarUrl" [alt]="selectedAuthor.nickname" class="monitor-hero-avatar" referrerpolicy="no-referrer" (error)="handleMonitorAuthorAvatarError(selectedAuthor)">
+
+                  <div class="monitor-hero-info">
+
+                    <div class="monitor-hero-title-row">
+
+                      <h3>{{selectedAuthor.nickname}}</h3>
+
+                      <span class="monitor-hero-verified" *ngIf="selectedAuthor.verifiedText">{{selectedAuthor.verifiedText}}</span>
+
+                    </div>
+
+                    <div class="monitor-hero-meta">
+
+                      <span>关注 {{selectedAuthor.followingCount | numberFormat}}</span>
+
+                      <span>粉丝 {{selectedAuthor.followerCount | numberFormat}}</span>
+
+                      <span>获赞 {{selectedAuthor.totalFavorited | numberFormat}}</span>
+
+                    </div>
+
+                    <div class="monitor-hero-submeta">
+
+                      <span>抖音号:{{selectedAuthor.uniqueId || selectedAuthor.shortId || selectedAuthor.uid}}</span>
+
+                      <span>{{getMonitorIpLocationText(selectedAuthor)}}</span>
+
+                    </div>
+
+                    <p class="monitor-hero-signature">{{selectedAuthor.signature || '该用户暂未填写简介'}}</p>
+
+                  </div>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="monitor-work-header">
+
+                <div>
+
+                  <h3>作品 {{selectedAuthor.awemeCount | numberFormat}}</h3>
+
+                  <p>已加载 {{selectedAuthor.works.length}} 条主页作品</p>
+
+                </div>
+
+                <div class="monitor-work-tools">
+
+                  <span class="monitor-work-status" *ngIf="selectedAuthor.loadingWorks">正在加载作品...</span>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="monitor-works-grid" *ngIf="selectedAuthor.works.length > 0">
+
+                <article class="monitor-work-card" *ngFor="let work of selectedAuthor.works">
+
+                  <div class="monitor-work-cover">
+
+                    <img [src]="work.coverUrl" [alt]="work.desc" referrerpolicy="no-referrer" (error)="handleMonitorWorkCoverError(work)">
+
+                    <span class="monitor-work-badge">{{work.type === 'image' ? '图文' : '视频'}}</span>
+
+                    <span class="monitor-work-duration" *ngIf="work.type === 'video' && work.duration">{{work.duration | timeFormat}}</span>
+
+                  </div>
+
+                  <div class="monitor-work-content">
+
+                    <p class="monitor-work-desc">{{work.desc}}</p>
+
+                    <div class="monitor-work-stats">
+
+                      <span>点赞 {{work.statistics.digg_count | numberFormat}}</span>
+
+                      <span>评论 {{work.statistics.comment_count | numberFormat}}</span>
+
+                    </div>
+
+                    <div class="monitor-work-date" *ngIf="work.createTime">{{work.createTime | date:'yyyy-MM-dd HH:mm'}}</div>
+
+
+
+                    <div class="monitor-work-actions" *ngIf="work.type === 'video'">
+
+                      <button class="btn" type="button" (click)="previewMonitorWork(selectedAuthor, work)">
+
+                        <span class="icon">visibility</span> 查看
+
+                      </button>
+
+                      <button class="btn btn-primary" type="button" (click)="downloadMonitorWork(selectedAuthor, work)" [disabled]="work.isDownloading">
+
+                        <span class="icon">{{ work.isDownloading ? 'hourglass_empty' : 'download' }}</span>
+
+                        {{ work.isDownloading ? '下载中 ' + (work.downloadProgress || 0) + '%' : '下载' }}
+
+                      </button>
+
+                    </div>
+
+
+
+                    <div class="monitor-work-progress" *ngIf="work.isDownloading">
+
+                      <div class="monitor-work-progress-bar">
+
+                        <div class="monitor-work-progress-fill" [style.width]="(work.downloadProgress || 0) + '%'"></div>
+
+                      </div>
+
+                    </div>
+
+                  </div>
+
+                </article>
+
+              </div>
+
+
+
+              <div class="empty-state" *ngIf="selectedAuthor.works.length === 0 && !selectedAuthor.loadingWorks">
+
+                <span class="icon large">perm_media</span>
+
+                <h3>暂无可展示作品</h3>
+
+                <p>这个博主暂时没有返回主页作品,或接口当前未取到数据</p>
+
+              </div>
+
+
+
+              <div class="monitor-load-more" *ngIf="selectedAuthor.hasMoreWorks">
+
+                <button class="btn btn-primary" (click)="loadMoreMonitorWorks(selectedAuthor)" [disabled]="selectedAuthor.loadingWorks">
+
+                  <span class="icon">refresh</span>
+
+                  {{selectedAuthor.loadingWorks ? '加载中...' : '加载更多作品'}}
+
+                </button>
+
+              </div>
+
+            </div>
+
+          </ng-container>
+
+        </ng-template>
+
+      </section>
+
+
+
+      <!-- 任务管理模块 -->
+
+      <section class="dh-page tasks-page" *ngIf="currentTab === 'tasks'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>任务管理</h1>
+
+            <p>监控和管理您的 AI 视频生成任务</p>
+
+          </div>
+
+        </header>
+
+
+
+        <!-- 顶部统计卡:可点击作为筛选器 -->
+
+        <div class="task-stats">
+
+          <button type="button" class="task-stat task-stat--pending"
+
+                  [class.task-stat--active]="tasksFilter === 'pending'"
+
+                  (click)="toggleTasksFilter('pending')">
+
+            <span class="task-stat__icon">
+
+              <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                <circle cx="12" cy="12" r="10"></circle>
+
+                <polyline points="12 6 12 12 16 14"></polyline>
+
+              </svg>
+
+            </span>
+
+            <div class="task-stat__body">
+
+              <span class="task-stat__number">{{taskStats.pending}}</span>
+
+              <span class="task-stat__label">等待中</span>
+
+            </div>
+
+          </button>
+
+          <button type="button" class="task-stat task-stat--processing"
+
+                  [class.task-stat--active]="tasksFilter === 'processing'"
+
+                  (click)="toggleTasksFilter('processing')">
+
+            <span class="task-stat__icon">
+
+              <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                <path d="M21 12a9 9 0 1 1-3-6.7L21 8"></path>
+
+                <polyline points="21 3 21 8 16 8"></polyline>
+
+              </svg>
+
+            </span>
+
+            <div class="task-stat__body">
+
+              <span class="task-stat__number">{{taskStats.processing}}</span>
+
+              <span class="task-stat__label">处理中</span>
+
+            </div>
+
+          </button>
+
+          <button type="button" class="task-stat task-stat--completed"
+
+                  [class.task-stat--active]="tasksFilter === 'completed'"
+
+                  (click)="toggleTasksFilter('completed')">
+
+            <span class="task-stat__icon">
+
+              <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
+
+                <polyline points="22 4 12 14.01 9 11.01"></polyline>
+
+              </svg>
+
+            </span>
+
+            <div class="task-stat__body">
+
+              <span class="task-stat__number">{{taskStats.completed}}</span>
+
+              <span class="task-stat__label">已完成</span>
+
+            </div>
+
+          </button>
+
+          <button type="button" class="task-stat task-stat--failed"
+
+                  [class.task-stat--active]="tasksFilter === 'failed'"
+
+                  (click)="toggleTasksFilter('failed')">
+
+            <span class="task-stat__icon">
+
+              <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                <circle cx="12" cy="12" r="10"></circle>
+
+                <line x1="15" y1="9" x2="9" y2="15"></line>
+
+                <line x1="9" y1="9" x2="15" y2="15"></line>
+
+              </svg>
+
+            </span>
+
+            <div class="task-stat__body">
+
+              <span class="task-stat__number">{{taskStats.failed}}</span>
+
+              <span class="task-stat__label">失败</span>
+
+            </div>
+
+          </button>
+
+        </div>
+
+
+
+        <!-- 任务列表 -->
+
+        <div class="task-list-section" *ngIf="activeTasks.length > 0">
+
+          <div class="task-list-section__head">
+
+            <h3 class="task-list-section__title">
+
+              任务列表
+
+              <span class="task-list-section__count">{{getFilteredTasks().length}}</span>
+
+            </h3>
+
+            <button type="button" class="task-btn task-btn--ghost" *ngIf="tasksFilter" (click)="toggleTasksFilter(tasksFilter)">
+
+              清除筛选
+
+            </button>
+
+          </div>
+
+
+
+          <div class="task-list" *ngIf="getFilteredTasks().length > 0">
+
+            <article class="task-row"
+
+                     *ngFor="let task of getFilteredTasks()"
+
+                     [class.task-row--processing]="task.status === 'processing'"
+
+                     [class.task-row--success]="task.status === 'completed'"
+
+                     [class.task-row--failed]="task.status === 'failed'">
+
+              <span class="task-row__accent" aria-hidden="true"></span>
+
+
+
+              <!-- 头部:标题 + 类型 + 状态 + 时间 -->
+
+              <div class="task-row__head">
+
+                <div class="task-row__title-wrap">
+
+                  <span class="task-row__type-badge" *ngIf="task.type === 'video-generation'">视频生成</span>
+
+                  <h4 class="task-row__title" [title]="task.original_video_title || task.keyword">
+
+                    {{task.original_video_title || task.keyword || '未命名任务'}}
+
+                  </h4>
+
+                </div>
+
+                <div class="task-row__head-right">
+
+                  <span class="task-row__status-pill"
+
+                        [class.task-row__status-pill--pending]="task.status === 'pending'"
+
+                        [class.task-row__status-pill--processing]="task.status === 'processing'"
+
+                        [class.task-row__status-pill--success]="task.status === 'completed'"
+
+                        [class.task-row__status-pill--failed]="task.status === 'failed'">
+
+                    <span class="task-row__status-dot"></span>
+
+                    {{getTaskStatusText(task.status)}}
+
+                  </span>
+
+                  <span class="task-row__date">
+
+                    <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                      <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
+
+                      <line x1="16" y1="2" x2="16" y2="6"></line>
+
+                      <line x1="8" y1="2" x2="8" y2="6"></line>
+
+                      <line x1="3" y1="10" x2="21" y2="10"></line>
+
+                    </svg>
+
+                    {{task.updated_at | date:'MM-dd HH:mm'}}
+
+                  </span>
+
+                </div>
+
+              </div>
+
+
+
+              <!-- 进度条 -->
+
+              <div class="task-row__progress">
+
+                <div class="task-row__progress-info">
+
+                  <span class="task-row__stage">{{getTaskStepText(task.step)}}</span>
+
+                  <span class="task-row__progress-pct">{{task.progress}}%</span>
+
+                </div>
+
+                <div class="task-row__progress-bar">
+
+                  <div class="task-row__progress-fill"
+
+                       [style.width]="task.progress + '%'"
+
+                       [class.task-row__progress-fill--success]="task.status === 'completed'"
+
+                       [class.task-row__progress-fill--failed]="task.status === 'failed'"></div>
+
+                </div>
+
+              </div>
+
+
+
+              <!-- 元信息 chips -->
+
+              <div class="task-row__meta" *ngIf="task.duration || task.keyword || task.vgData">
+
+                <span class="task-row__chip" *ngIf="task.duration">
+
+                  <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <polygon points="23 7 16 12 23 17 23 7"></polygon>
+
+                    <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
+
+                  </svg>
+
+                  {{task.duration}}
+
+                </span>
+
+                <span class="task-row__chip" *ngIf="task.keyword && (task.original_video_title && task.original_video_title !== task.keyword)">
+
+                  <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <circle cx="12" cy="12" r="10"></circle>
+
+                    <circle cx="12" cy="12" r="4"></circle>
+
+                  </svg>
+
+                  {{task.keyword}}
+
+                </span>
+
+                <span class="task-row__chip" *ngIf="task.type === 'video-generation' && task.vgData">
+
+                  <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
+
+                    <polyline points="2 17 12 22 22 17"></polyline>
+
+                    <polyline points="2 12 12 17 22 12"></polyline>
+
+                  </svg>
+
+                  步骤 {{task.vgData.step}}/6
+
+                </span>
+
+                <span class="task-row__chip" *ngIf="task.type === 'video-generation' && task.vgData?.scriptSegments?.length">
+
+                  <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
+
+                    <polyline points="14 2 14 8 20 8"></polyline>
+
+                  </svg>
+
+                  {{task.vgData?.scriptSegments?.length}} 个片段
+
+                </span>
+
+              </div>
+
+
+
+              <!-- 错误信息 -->
+
+              <div class="task-row__error" *ngIf="task.status === 'failed' && task.error_message">
+
+                <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                  <circle cx="12" cy="12" r="10"></circle>
+
+                  <line x1="12" y1="8" x2="12" y2="12"></line>
+
+                  <line x1="12" y1="16" x2="12.01" y2="16"></line>
+
+                </svg>
+
+                <span>{{task.error_message}}</span>
+
+              </div>
+
+
+
+              <!-- 操作按钮 -->
+
+              <div class="task-row__actions">
+
+                <a *ngIf="task.status === 'completed' && task.result_url" [href]="task.result_url" target="_blank"
+
+                   class="task-btn task-btn--success">
+
+                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <polygon points="5 3 19 12 5 21 5 3"></polygon>
+
+                  </svg>
+
+                  查看结果
+
+                </a>
+
+                <button type="button" class="task-btn task-btn--primary"
+
+                        *ngIf="task.type === 'video-generation' && task.vgData && task.status !== 'completed'"
+
+                        (click)="vgResumeFromTask(task)">
+
+                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <polygon points="5 3 19 12 5 21 5 3"></polygon>
+
+                  </svg>
+
+                  继续任务
+
+                </button>
+
+                <button type="button" class="task-btn task-btn--primary"
+
+                        *ngIf="task.type === 'video-generation' && task.vgData && task.status === 'completed'"
+
+                        (click)="vgResumeFromTask(task)">
+
+                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+
+                    <circle cx="12" cy="12" r="3"></circle>
+
+                  </svg>
+
+                  查看详情
+
+                </button>
+
+                <button type="button" class="task-btn task-btn--ghost" *ngIf="task.status === 'processing'" (click)="cancelTask(task)">
+
+                  取消
+
+                </button>
+
+                <button type="button" class="task-btn task-btn--danger" (click)="removeTask(task.id)" title="删除任务">
+
+                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <polyline points="3 6 5 6 21 6"></polyline>
+
+                    <path d="M19 6l-1.5 14a2 2 0 0 1-2 1.5H8.5a2 2 0 0 1-2-1.5L5 6"></path>
+
+                    <path d="M10 11v6M14 11v6"></path>
+
+                  </svg>
+
+                </button>
+
+              </div>
+
+            </article>
+
+          </div>
+
+
+
+          <!-- 筛选后无结果 -->
+
+          <div class="task-empty task-empty--inline" *ngIf="getFilteredTasks().length === 0">
+
+            <p>当前筛选条件下没有匹配的任务</p>
+
+            <button type="button" class="task-btn task-btn--ghost" (click)="toggleTasksFilter(tasksFilter)">
+
+              清除筛选
+
+            </button>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="task-empty" *ngIf="activeTasks.length === 0">
+
+          <div class="task-empty__icon">
+
+            <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+
+              <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
+
+              <polyline points="14 2 14 8 20 8"></polyline>
+
+              <line x1="16" y1="13" x2="8" y2="13"></line>
+
+              <line x1="16" y1="17" x2="8" y2="17"></line>
+
+              <polyline points="10 9 9 9 8 9"></polyline>
+
+            </svg>
+
+          </div>
+
+          <h3>暂无任务</h3>
+
+          <p>在视频生成或视频管理中创建任务,任务将显示在这里</p>
+
+        </div>
+
+      </section>
+
+
+
+      <!-- 生成历史模块 -->
+
+      <section class="dh-page history-page" *ngIf="currentTab === 'history'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <path d="M3 12a9 9 0 1 0 9-9"></path>
+
+              <polyline points="3 4 3 9 8 9"></polyline>
+
+              <polyline points="12 7 12 12 15 14"></polyline>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>生成历史</h1>
+
+            <p>所有 AI 生成任务的完整记录</p>
+
+          </div>
+
+        </header>
+
+        <div class="history-toolbar">
+
+          <div class="history-toolbar__filter" role="tablist">
+
+            <button type="button" class="history-pill"
+
+                    [class.history-pill--active]="!historyFilter.status"
+
+                    (click)="historyFilter.status = ''">
+
+              全部 <span class="history-pill__count">{{getHistoryStatusCount('')}}</span>
+
+            </button>
+
+            <button type="button" class="history-pill history-pill--success"
+
+                    [class.history-pill--active]="historyFilter.status === 'completed'"
+
+                    (click)="historyFilter.status = 'completed'">
+
+              已完成 <span class="history-pill__count">{{getHistoryStatusCount('completed')}}</span>
+
+            </button>
+
+            <button type="button" class="history-pill history-pill--danger"
+
+                    [class.history-pill--active]="historyFilter.status === 'failed'"
+
+                    (click)="historyFilter.status = 'failed'">
+
+              失败 <span class="history-pill__count">{{getHistoryStatusCount('failed')}}</span>
+
+            </button>
+
+          </div>
+
+          <button type="button" class="history-export" (click)="exportHistory()" title="导出全部记录为 JSON">
+
+            <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+
+              <polyline points="7 10 12 15 17 10"></polyline>
+
+              <line x1="12" y1="15" x2="12" y2="3"></line>
+
+            </svg>
+
+            <span>导出记录</span>
+
+          </button>
+
+        </div>
+
+
+
+        <div class="history-list" *ngIf="getFilteredHistoryRecords().length > 0">
+
+          <ng-container *ngFor="let record of getFilteredHistoryRecords()">
+
+            <article class="history-card"
+
+                     [class.history-card--success]="record.status === 'completed'"
+
+                     [class.history-card--failed]="record.status === 'failed'"
+
+                     *ngIf="parseHistoryRecordTitle(record) as parsed">
+
+              <span class="history-card__accent" aria-hidden="true"></span>
+
+
+
+              <div class="history-card__body">
+
+                <div class="history-card__head">
+
+                  <h4 class="history-card__title">{{parsed.title}}</h4>
+
+                  <span class="history-card__date">
+
+                    <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                      <rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
+
+                      <line x1="16" y1="2" x2="16" y2="6"></line>
+
+                      <line x1="8" y1="2" x2="8" y2="6"></line>
+
+                      <line x1="3" y1="10" x2="21" y2="10"></line>
+
+                    </svg>
+
+                    {{record.created_at | date:'yyyy-MM-dd HH:mm'}}
+
+                  </span>
+
+                </div>
+
+
+
+                <div class="history-card__tags" *ngIf="parsed.tags.length > 0">
+
+                  <span class="history-tag" *ngFor="let t of parsed.tags">#{{t}}</span>
+
+                </div>
+
+
+
+                <div class="history-card__meta">
+
+                  <span class="history-status-pill"
+
+                        [class.history-status-pill--success]="record.status === 'completed'"
+
+                        [class.history-status-pill--failed]="record.status === 'failed'">
+
+                    <span class="history-status-pill__dot"></span>
+
+                    {{getStatusText(record.status)}}
+
+                  </span>
+
+                  <span class="history-chip" *ngIf="record.styleName">风格 · {{record.styleName}}</span>
+
+                  <span class="history-chip" *ngIf="record.successCount !== undefined">片段 · {{record.successCount}}/{{record.segmentCount}}</span>
+
+                  <span class="history-chip" *ngIf="record.audioSourceLabel">音频 · {{record.audioSourceLabel}}</span>
+
+                  <span class="history-chip" *ngIf="record.duration">{{record.duration}}</span>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="history-card__cost">
+
+                <span class="history-card__cost-label">费用</span>
+
+                <span class="history-card__cost-value">¥{{record.cost || 0}}</span>
+
+              </div>
+
+
+
+              <div class="history-card__actions">
+
+                <button type="button" class="history-btn history-btn--primary" (click)="viewHistoryDetail(record)">
+
+                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
+
+                    <circle cx="12" cy="12" r="3"></circle>
+
+                  </svg>
+
+                  查看详情
+
+                </button>
+
+                <button type="button" class="history-btn history-btn--ghost" (click)="removeHistoryRecord(record.id)" title="删除该条记录">
+
+                  <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                    <polyline points="3 6 5 6 21 6"></polyline>
+
+                    <path d="M19 6l-1.5 14a2 2 0 0 1-2 1.5H8.5a2 2 0 0 1-2-1.5L5 6"></path>
+
+                    <path d="M10 11v6M14 11v6"></path>
+
+                    <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path>
+
+                  </svg>
+
+                </button>
+
+              </div>
+
+            </article>
+
+          </ng-container>
+
+        </div>
+
+
+
+        <div class="history-empty" *ngIf="getFilteredHistoryRecords().length === 0">
+
+          <div class="history-empty__icon">
+
+            <svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+
+              <path d="M3 12a9 9 0 1 0 9-9"></path>
+
+              <polyline points="3 4 3 9 8 9"></polyline>
+
+              <polyline points="12 7 12 12 15 14"></polyline>
+
+            </svg>
+
+          </div>
+
+          <h3>{{historyRecords.length === 0 ? '暂无历史记录' : '没有符合筛选条件的记录'}}</h3>
+
+          <p>{{historyRecords.length === 0 ? '完成第一个 AI 生成任务后,记录将显示在这里' : '试试切换其他状态标签,或清除当前筛选'}}</p>
+
+          <button type="button" class="history-btn history-btn--ghost" *ngIf="historyRecords.length > 0 && historyFilter.status" (click)="historyFilter.status = ''">
+
+            清除筛选
+
+          </button>
+
+        </div>
+
+      </section>
+
+
+
+      <!-- 结果展示模块 -->
+
+      <section class="dh-page results-page" *ngIf="currentTab === 'results'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
+
+              <polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none"></polygon>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>结果展示</h1>
+
+            <p>查看和下载您的 AI 生成内容</p>
+
+          </div>
+
+        </header>
+
+
+
+        <div class="results-grid" *ngIf="generatedResults.length > 0">
+
+          <div class="result-card" *ngFor="let result of generatedResults">
+
+            <div class="result-media">
+
+              <div class="video-preview" *ngIf="result.type === 'video' && result.url">
+
+                <video controls [src]="result.url" preload="metadata"></video>
+
+                <div class="result-badge">
+
+                  <span class="icon">play_circle</span>
+
+                  <span>{{result.quality || '视频'}}</span>
+
+                </div>
+
+              </div>
+
+              <div class="image-preview" *ngIf="result.type === 'image'">
+
+                <img [src]="result.url" [alt]="result.title">
+
+                <div class="result-badge">
+
+                  <span class="icon">image</span>
+
+                  <span>图片</span>
+
+                </div>
+
+              </div>
+
+              <div *ngIf="result.type === 'video' && !result.url" style="padding:24px;text-align:center;color:var(--text-secondary);background:var(--bg-tertiary);border-radius:8px">
+
+                <span class="icon" style="font-size:36px;display:block;margin-bottom:8px">videocam_off</span>
+
+                <span>无主视频(查看下方片段)</span>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="result-content">
+
+              <h4 class="result-title">{{result.title}}</h4>
+
+              <div class="result-meta">
+
+                <span class="result-date">{{result.created_at | date:'yyyy-MM-dd HH:mm'}}</span>
+
+                <span class="result-quality">{{result.quality}}</span>
+
+              </div>
+
+              <div class="result-stats">
+
+                <span class="stat-item">
+
+                  <span class="icon">video_library</span>
+
+                  <span>{{result.duration}}</span>
+
+                </span>
+
+                <span class="stat-item">
+
+                  <span class="icon">attach_money</span>
+
+                  <span>费用: ¥{{result.cost}}</span>
+
+                </span>
+
+              </div>
+
+
+
+              <!-- 片段视频列表 -->
+
+              <div *ngIf="result.segments && result.segments.length > 0" style="margin-top:10px">
+
+                <details>
+
+                  <summary style="cursor:pointer;font-size:13px;color:var(--accent-color);user-select:none">
+
+                    <span class="icon" style="font-size:14px;vertical-align:middle">expand_more</span>
+
+                    展开全部 {{result.segments.length}} 个片段
+
+                  </summary>
+
+                  <div style="margin-top:8px;display:flex;flex-direction:column;gap:8px">
+
+                    <div *ngFor="let seg of result.segments" style="padding:8px;background:var(--bg-tertiary);border-radius:6px">
+
+                      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
+
+                        <span style="font-size:12px;font-weight:600">{{seg.id}}</span>
+
+                        <span class="status-badge badge-success" style="font-size:11px">成功</span>
+
+                      </div>
+
+                      <video controls [src]="seg.videoUrl" preload="metadata" style="width:100%;max-height:200px;border-radius:4px;background:#000"></video>
+
+                      <a [href]="seg.videoUrl" target="_blank" style="display:inline-block;margin-top:4px;font-size:12px;color:var(--accent-color)">
+
+                        <span class="icon" style="font-size:12px">open_in_new</span> 新窗口
+
+                      </a>
+
+                    </div>
+
+                  </div>
+
+                </details>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="result-actions">
+
+              <button class="btn" (click)="previewResult(result)" *ngIf="result.url">
+
+                <span class="icon">visibility</span> 预览
+
+              </button>
+
+              <button class="btn btn-primary" (click)="downloadResult(result)" *ngIf="result.url">
+
+                <span class="icon">download</span> 下载
+
+              </button>
+
+              <button class="btn" (click)="removeResult(result.id)">
+
+                <span class="icon">delete</span> 删除
+
+              </button>
+
+            </div>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="empty-state" *ngIf="generatedResults.length === 0">
+
+          <span class="icon large">video_library</span>
+
+          <h3>暂无生成结果</h3>
+
+          <p>完成AI生成任务后,结果将显示在这里</p>
+
+        </div>
+
+      </section>
+
+
+
+      <!-- 视频管理模块 -->
+
+      <section class="dh-page videos-page" *ngIf="currentTab === 'videos'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>视频管理</h1>
+
+            <p>本地已下载与生成的视频素材库</p>
+
+          </div>
+
+        </header>
+
+        <div class="videos-header videos-toolbar-row">
+
+          <div class="videos-controls">
+
+            <div class="form-field filter-field">
+
+              <label>分类筛选</label>
+
+              <select [(ngModel)]="videoFilter.category">
+
+                <option value="">全部分类</option>
+
+                <option *ngFor="let category of videoCategories" [value]="category.id">
+
+                  {{category.name}} ({{category.count}})
+
+                </option>
+
+              </select>
+
+            </div>
+
+            <div class="form-field filter-field">
+
+              <label>来源</label>
+
+              <select [(ngModel)]="videoFilter.source">
+
+                <option value="">全部来源</option>
+
+                <option value="downloaded">下载</option>
+
+                <option value="generated">AI生成</option>
+
+                <option value="uploaded">上传</option>
+
+              </select>
+
+            </div>
+
+            <div class="form-field search-field">
+
+              <label>搜索视频</label>
+
+              <input type="text" placeholder="输入关键词搜索..." [(ngModel)]="videoFilter.searchTerm">
+
+            </div>
+
+            <button class="btn btn-primary" type="button" (click)="openUploadModal()" style="position:relative;z-index:10">
+
+              <span class="icon">upload</span> 上传视频
+
+            </button>
+
+            <button class="btn" (click)="isSelectionMode = !isSelectionMode">
+
+              <span class="icon">{{isSelectionMode ? 'close' : 'check_circle'}}</span>
+
+              {{isSelectionMode ? '取消选择' : '批量操作'}}
+
+            </button>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="videos-actions" *ngIf="isSelectionMode && selectedVideos.size > 0">
+
+          <div class="selection-info">
+
+            已选择 {{selectedVideos.size}} 个视频
+
+          </div>
+
+          <div class="batch-actions">
+
+            <button class="btn" (click)="batchDelete()">
+
+              <span class="icon">delete</span> 批量删除
+
+            </button>
+
+            <button class="btn" (click)="batchExport()">
+
+              <span class="icon">download</span> 批量导出
+
+            </button>
+
+            <button class="btn" (click)="batchCategorize()">
+
+              <span class="icon">folder</span> 批量分类
+
+            </button>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="videos-grid" *ngIf="getFilteredVideos().length > 0">
+
+          <div class="video-item" *ngFor="let video of getFilteredVideos()" [class.selected]="selectedVideos.has(video.id)">
+
+            <div class="video-checkbox" *ngIf="isSelectionMode">
+
+              <input type="checkbox" [checked]="selectedVideos.has(video.id)" (change)="toggleVideoSelection(video.id)">
+
+            </div>
+
+
+
+            <div class="video-thumbnail">
+
+              <img *ngIf="video.thumbnail" [src]="video.thumbnail" [alt]="video.title">
+
+              <video *ngIf="!video.thumbnail && getVideoPlayUrl(video)" [src]="getVideoPlayUrl(video) + '#t=0.1'" muted preload="metadata" style="width:100%;height:100%;object-fit:cover;pointer-events:none"></video>
+
+              <div *ngIf="!video.thumbnail && !getVideoPlayUrl(video)" class="video-thumb-placeholder">
+
+                <span class="icon" style="font-size:36px;color:var(--text-muted)">movie</span>
+
+              </div>
+
+              <div class="video-duration">{{video.duration | timeFormat}}</div>
+
+              <div class="video-source" [class]="video.source">{{getSourceText(video.source)}}</div>
+
+              <!-- AI重塑进度指示器 -->
+
+              <div class="remix-progress" *ngIf="video.isRemixing">
+
+                <div class="remix-progress-bar">
+
+                  <div class="remix-progress-fill" [style.width]="video.remixProgress + '%'"></div>
+
+                </div>
+
+                <div class="remix-style">{{video.remixStyle}}</div>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="video-info">
+
+              <div class="video-title-row">
+
+                <h4 class="video-title">{{video.title}}</h4>
+
+                <span class="category-badge" [style.background-color]="getCategoryColor(video.category)">
+
+                  {{getCategoryName(video.category)}}
+
+                </span>
+
+              </div>
+
+
+
+              <div class="video-meta">
+
+                <span class="meta-item">
+
+                  <span class="icon">schedule</span>
+
+                  {{video.created_at | date:'yyyy-MM-dd HH:mm'}}
+
+                </span>
+
+                <span class="meta-item">
+
+                  <span class="icon">storage</span>
+
+                  {{video.size | fileSize}}
+
+                </span>
+
+                <span class="meta-item">
+
+                  <span class="icon">hd</span>
+
+                  {{video.metadata.resolution}}
+
+                </span>
+
+              </div>
+
+
+
+              <div class="video-description" *ngIf="video.description">
+
+                {{video.description}}
+
+              </div>
+
+
+
+              <div class="video-tags" *ngIf="video.tags.length > 0">
+
+                <span class="tag" *ngFor="let tag of video.tags">
+
+                  #{{tag}}
+
+                </span>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="video-actions">
+
+              <button class="btn" (click)="previewVideo(video)">
+
+                <span class="icon">play_arrow</span> 预览
+
+              </button>
+
+              <button class="btn" (click)="editVideo(video)">
+
+                <span class="icon">edit</span> 编辑
+
+              </button>
+
+              <button class="btn {{getRemixStatusColor(video)}}" (click)="openRemixModeChooser(video)" [disabled]="video.isRemixing">
+
+                <span class="icon">
+
+                  {{video.isRemixing ? 'hourglass_empty' : 'auto_awesome'}}
+
+                </span>
+
+                {{getRemixStatusText(video)}}
+
+              </button>
+
+              <button class="btn" (click)="viewRemixVersions(video)" *ngIf="video.remixVersions && video.remixVersions.length > 0">
+
+                <span class="icon">history</span> 版本
+
+              </button>
+
+              <button class="btn btn-warn" (click)="deleteVideo(video)">
+
+                <span class="icon">delete</span> 删除
+
+              </button>
+
+            </div>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="empty-state" *ngIf="getFilteredVideos().length === 0">
+
+          <span class="icon large">folder_open</span>
+
+          <h3>暂无视频文件</h3>
+
+          <p>下载、生成或上传视频后,它们将显示在这里进行管理</p>
+
+          <button class="btn btn-primary" style="margin-top:12px" (click)="openUploadModal()">
+
+            <span class="icon">upload</span> 上传视频
+
+          </button>
+
+        </div>
+
+      </section>
+
+
+
+      <!-- 语音合成模块(重设计:二栏布局) -->
+
+      <section class="dh-page vs-page" *ngIf="currentTab === 'voice-synthesis'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
+
+              <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
+
+              <line x1="12" y1="19" x2="12" y2="23"></line>
+
+              <line x1="8" y1="23" x2="16" y2="23"></line>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>声音定制</h1>
+
+            <p>上传音频样本训练专属音色,或选择已有音色合成个性化语音</p>
+
+          </div>
+
+          <button type="button" class="dh-help-btn" (click)="showToast('💡 先在左侧「声音训练」上传样本完成复刻,再到右侧「声音合成」选择音色生成语音', 'info', 5000)">
+
+            <span class="dh-help-icon">?</span>
+
+            <span>帮助</span>
+
+          </button>
+
+        </header>
+
+
+
+        <div class="vs-two-col">
+
+
+
+          <!-- ========== 左:声音训练(音色复刻) ========== -->
+
+          <article class="dh-card vs-subcard">
+
+            <header class="vs-subcard-header">
+
+              <div class="vs-subcard-icon vs-subcard-icon--purple">
+
+                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                  <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
+
+                  <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
+
+                  <line x1="12" y1="19" x2="12" y2="23"></line>
+
+                  <line x1="8" y1="23" x2="16" y2="23"></line>
+
+                </svg>
+
+              </div>
+
+              <div>
+
+                <h2>声音训练</h2>
+
+                <p>上传音频样本,训练专属声音模型</p>
+
+              </div>
+
+              <button type="button" class="dh-help-btn">
+
+                <span class="dh-help-icon">?</span>
+
+                <span>帮助</span>
+
+              </button>
+
+            </header>
+
+
+
+            <div class="dh-segment vs-tab-header">
+
+              <button type="button" class="dh-segment__item is-active">上传音频</button>
+
+              <button type="button" class="dh-segment__item" disabled title="即将上线">直接录音</button>
+
+            </div>
+
+
+
+            <input type="file" id="vsCloneAudioInput"
+
+                   accept=".mp3,.wav,.m4a,.ogg,.aac,.flac,.wma,audio/*"
+
+                   (change)="onVsCloneAudioSelected($event)"
+
+                   hidden
+
+                   [disabled]="vsCloneLoading || vsCloneUploading">
+
+
+
+            <label for="vsCloneAudioInput" class="dh-upload__zone vs-audio-upload" *ngIf="!vsCloneAudioName">
+
+              <svg class="dh-upload__icon" viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+
+                <path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
+
+                <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
+
+              </svg>
+
+              <h3 class="dh-upload__title">点击或拖拽音频文件到此区域</h3>
+
+              <ul class="dh-upload__hints">
+
+                <li>支持 MP3 / WAV / M4A / OGG / AAC / FLAC 等格式</li>
+
+                <li>建议时长 5-30 秒,文件大小不超过 50MB</li>
+
+                <li>单人纯净人声、无背景音乐和噪声效果最佳</li>
+
+              </ul>
+
+            </label>
+
+
+
+            <div class="vs-upload-filled" *ngIf="vsCloneAudioName">
+
+              <div style="min-width:0;flex:1">
+
+                <div class="dh-upload__preview-name">{{vsCloneAudioName}}</div>
+
+                <div class="dh-upload__preview-size">
+
+                  {{vsCloneAudioFile ? (vsCloneAudioFile.size / 1024 / 1024).toFixed(2) + ' MB' : ''}}
+
+                  <span *ngIf="vsCloneAudioUrl" style="color:var(--dh-success)"> · 已上传</span>
+
+                  <span *ngIf="vsCloneUploading" style="color:#c4b5fd"> · 正在上传...</span>
+
+                </div>
+
+              </div>
+
+              <div class="dh-upload__preview-actions" *ngIf="!vsCloneLoading && !vsCloneUploading">
+
+                <label for="vsCloneAudioInput" class="dh-chip-btn">更换</label>
+
+                <button type="button" class="dh-chip-btn dh-chip-btn--danger" (click)="clearVsCloneAudio()">移除</button>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="dh-field">
+
+              <label class="dh-field__label">
+
+                <span>声音名称 <em>*</em></span>
+
+                <span class="dh-field__counter">{{(vsCloneForm.name || '').length}} / 20</span>
+
+              </label>
+
+              <input class="dh-input" type="text" maxlength="20" [(ngModel)]="vsCloneForm.name"
+
+                     placeholder="为这个声音起个名字"
+
+                     [disabled]="vsCloneLoading || vsCloneUploading">
+
+              <p class="vs-hint">起个名字方便记忆(20 字以内),例如:我的声音</p>
+
+            </div>
+
+
+
+            <div class="dh-field-row">
+
+              <div class="dh-field" style="flex:2;min-width:0">
+
+                <label class="dh-field__label"><span>Speaker ID</span></label>
+
+                <select class="dh-input" [(ngModel)]="vsCloneForm.speakerId" [disabled]="vsCloneLoading || vsCloneUploading">
+
+                  <option value="S_ULZKhZWZ1">S_ULZKhZWZ1</option>
+
+                  <option value="S_TLZKhZWZ1">S_TLZKhZWZ1</option>
+
+                  <option value="S_SLZKhZWZ1">S_SLZKhZWZ1</option>
+
+                  <option value="S_QLZKhZWZ1">S_QLZKhZWZ1</option>
+
+                  <option value="S_PLZKhZWZ1">S_PLZKhZWZ1</option>
+
+                  <option value="S_OLZKhZWZ1">S_OLZKhZWZ1</option>
+
+                  <option value="S_MLZKhZWZ1">S_MLZKhZWZ1</option>
+
+                  <option value="S_RWDjSzXZ1">S_RWDjSzXZ1</option>
+
+                </select>
+
+              </div>
+
+              <div class="dh-field" style="flex:1;min-width:0">
+
+                <label class="dh-field__label"><span>语言</span></label>
+
+                <select class="dh-input" [(ngModel)]="vsCloneForm.language" [disabled]="vsCloneLoading || vsCloneUploading">
+
+                  <option [ngValue]="0">中文</option>
+
+                  <option [ngValue]="1">英文</option>
+
+                </select>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="dh-field">
+
+              <label class="dh-field__label"><span>音频对应文本(可选)</span></label>
+
+              <input class="dh-input" type="text" [(ngModel)]="vsCloneForm.audioText"
+
+                     placeholder="音频中说的文本内容,有助于提升复刻质量"
+
+                     [disabled]="vsCloneLoading || vsCloneUploading">
+
+            </div>
+
+
+
+            <div class="dh-field">
+
+              <label class="dh-field__label"><span>试听文本(4-80 字) <em>*</em></span></label>
+
+              <input class="dh-input" type="text" [(ngModel)]="vsCloneForm.demoText"
+
+                     placeholder="复刻成功后用于生成试听音频的文本"
+
+                     [disabled]="vsCloneLoading || vsCloneUploading">
+
+            </div>
+
+
+
+            <div class="dh-error" *ngIf="vsCloneError">{{vsCloneError}}</div>
+
+
+
+            <button type="button" class="dh-cta" (click)="vsStartClone()" [disabled]="vsCloneLoading || vsCloneUploading">
+
+              <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                <polygon points="5 3 19 12 5 21 5 3"></polygon>
+
+              </svg>
+
+              <span>{{vsCloneUploading ? '上传音频中...' : vsCloneLoading ? '复刻中...' : '开始训练'}}</span>
+
+            </button>
+
+
+
+            <p class="vs-hint" style="text-align:center"
+
+               *ngIf="!vsCloneLoading && !vsCloneUploading && (!vsCloneForm.name || !vsCloneAudioName)">
+
+              {{ !vsCloneForm.name ? '请输入声音名称' : '请上传或录制音频' }}
+
+            </p>
+
+
+
+            <!-- 复刻结果 -->
+
+            <div class="vs-result" *ngIf="vsCloneResult?.code === 200 && vsCloneResult?.data?.timbre">
+
+              <div class="vs-result-head">
+
+                <span class="dh-status-banner__check">✓</span>
+
+                <span>复刻成功</span>
+
+              </div>
+
+              <div class="vs-result-info">
+
+                <div><strong>音色 ID:</strong><code style="user-select:all">{{vsCloneResult.data.timbre.objectId}}</code></div>
+
+                <div><strong>音色名称:</strong>{{vsCloneResult.data.timbre.name}}</div>
+
+                <div><strong>Speaker ID:</strong>{{vsCloneResult.data.timbre.speaker_id}}</div>
+
+                <div *ngIf="vsCloneResult.data.timbre.models?.[0]?.demo_audio">
+
+                  <strong>试听:</strong>
+
+                  <audio controls [src]="vsCloneResult.data.timbre.models[0].demo_audio" preload="metadata" class="vs-audio-compact"></audio>
+
+                </div>
+
+              </div>
+
+              <button type="button" class="dh-link-btn" style="margin-top:10px"
+
+                      (click)="vsSelectClonedTimbre(vsCloneResult.data.timbre.objectId)">
+
+                使用此音色合成 →
+
+              </button>
+
+            </div>
+
+
+
+            <!-- 已复刻快捷列表 -->
+
+            <div class="vs-cloned-list" *ngIf="vsClonedTimbreList.length > 0">
+
+              <h4 class="vs-subtle-title">已复刻音色 ({{vsClonedTimbreList.length}})</h4>
+
+              <div class="dh-chips">
+
+                <button type="button" class="dh-chip" *ngFor="let t of vsClonedTimbreList"
+
+                        (click)="vsSelectClonedTimbre(t.objectId)">
+
+                  {{t.name}}
+
+                </button>
+
+              </div>
+
+            </div>
+
+          </article>
+
+
+
+          <!-- ========== 右:声音合成 ========== -->
+
+          <article class="dh-card vs-subcard">
+
+            <header class="vs-subcard-header">
+
+              <div class="vs-subcard-icon vs-subcard-icon--pink">
+
+                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                  <path d="M3 10v3M7 6v11M11 3v17M15 7v9M19 10v3"></path>
+
+                </svg>
+
+              </div>
+
+              <div>
+
+                <h2>声音合成</h2>
+
+                <p>选择声音模型,生成个性化语音</p>
+
+              </div>
+
+              <button type="button" class="dh-help-btn">
+
+                <span class="dh-help-icon">?</span>
+
+                <span>帮助</span>
+
+              </button>
+
+            </header>
+
+
+
+            <div class="dh-field">
+
+              <label class="dh-field__label"><span>选择声音模型</span></label>
+
+              <div class="vs-select-row">
+
+                <select class="dh-input" [(ngModel)]="vsSynthForm.timbreId" [disabled]="vsSynthLoading">
+
+                  <option value="">请选择音色...</option>
+
+                  <option *ngFor="let t of vsClonedTimbreList" [value]="t.objectId">{{t.name || t.objectId}}</option>
+
+                </select>
+
+                <button type="button" class="vs-preview-btn" title="试听(选择音色后可用)">▶</button>
+
+              </div>
+
+              <div class="dh-chips" *ngIf="vsClonedTimbreList.length > 0">
+
+                <button type="button" class="dh-chip" *ngFor="let t of vsClonedTimbreList"
+
+                        [class.is-active]="vsSynthForm.timbreId === t.objectId"
+
+                        (click)="vsSynthForm.timbreId = t.objectId"
+
+                        [disabled]="vsSynthLoading">
+
+                  {{t.name}}
+
+                </button>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="dh-field">
+
+              <label class="dh-field__label">
+
+                <span>输入文本</span>
+
+                <span class="dh-field__counter">{{(vsSynthForm.text || '').length}} / 5000</span>
+
+              </label>
+
+              <textarea class="dh-textarea" rows="4" maxlength="5000" [(ngModel)]="vsSynthForm.text"
+
+                        placeholder="你好,这是一个声音克隆测试。"
+
+                        [disabled]="vsSynthLoading"></textarea>
+
+            </div>
+
+
+
+            <div class="dh-field-row">
+
+              <div class="dh-field" style="flex:1;min-width:0">
+
+                <label class="dh-field__label">
+
+                  <span>语速</span>
+
+                  <span class="dh-field__counter">{{vsSynthForm.speechRate}}</span>
+
+                </label>
+
+                <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.speechRate" [disabled]="vsSynthLoading">
+
+              </div>
+
+              <div class="dh-field" style="flex:1;min-width:0">
+
+                <label class="dh-field__label">
+
+                  <span>音量</span>
+
+                  <span class="dh-field__counter">{{vsSynthForm.loudnessRate}}</span>
+
+                </label>
+
+                <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.loudnessRate" [disabled]="vsSynthLoading">
+
+              </div>
+
+            </div>
+
+
+
+            <div class="dh-field-row">
+
+              <div class="dh-field" style="flex:1;min-width:0">
+
+                <label class="dh-field__label"><span>音频格式</span></label>
+
+                <select class="dh-input" [(ngModel)]="vsSynthForm.format" [disabled]="vsSynthLoading">
+
+                  <option value="mp3">MP3</option>
+
+                  <option value="ogg_opus">OGG Opus</option>
+
+                  <option value="pcm">PCM</option>
+
+                </select>
+
+              </div>
+
+              <div class="dh-field" style="flex:1;min-width:0">
+
+                <label class="dh-field__label"><span>采样率</span></label>
+
+                <select class="dh-input" [(ngModel)]="vsSynthForm.sampleRate" [disabled]="vsSynthLoading">
+
+                  <option [ngValue]="8000">8000 Hz</option>
+
+                  <option [ngValue]="16000">16000 Hz</option>
+
+                  <option [ngValue]="24000">24000 Hz</option>
+
+                  <option [ngValue]="48000">48000 Hz</option>
+
+                </select>
+
+              </div>
+
+            </div>
+
+
+
+            <div class="dh-error" *ngIf="vsSynthError">{{vsSynthError}}</div>
+
+
+
+            <button type="button" class="dh-cta" (click)="vsStartSynthesize()" [disabled]="vsSynthLoading">
+
+              <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+                <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
+
+                <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
+
+              </svg>
+
+              <span>{{vsSynthLoading ? '合成中...' : '生成语音'}}</span>
+
+            </button>
+
+
+
+            <p class="vs-hint" style="text-align:center"
+
+               *ngIf="!vsSynthLoading && (!vsSynthForm.timbreId || !vsSynthForm.text)">
+
+              {{ !vsSynthForm.timbreId ? '请选择声音模型' : '请输入要合成的文本' }}
+
+            </p>
+
+
+
+            <!-- 合成结果 -->
+
+            <div class="vs-result" *ngIf="vsSynthAudioUrl">
+
+              <div class="vs-result-head">
+
+                <span class="dh-status-banner__check">✓</span>
+
+                <span>合成成功</span>
+
+              </div>
+
+              <audio controls [src]="vsSynthAudioUrl" preload="metadata" class="vs-audio-full"></audio>
+
+              <div class="vs-result-info">
+
+                <div *ngIf="vsSynthWorkId"><strong>作品 ID:</strong><code>{{vsSynthWorkId}}</code></div>
+
+              </div>
+
+              <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
+
+                <a [href]="vsSynthAudioUrl" target="_blank" class="dh-link-btn dh-link-btn--ghost">新窗口播放</a>
+
+                <a [href]="vsSynthAudioUrl" download class="dh-link-btn">下载音频</a>
+
+              </div>
+
+            </div>
+
+          </article>
+
+
+
+        </div>
+
+
+
+        <!-- 接口说明 footer -->
+
+        <footer class="vs-api-footer">
+
+          <h4>接口说明</h4>
+
+          <div><strong>音色复刻:</strong><code>POST https://server.fmode.cn/api/volcengine/tts/voice_clone</code></div>
+
+          <div><strong>语音合成:</strong><code>POST https://server.fmode.cn/api/volcengine/tts/unidirectional</code></div>
+
+          <div><strong>计费说明:</strong>音色复刻按次计费,语音合成每次 1 credit(0.05 元)</div>
+
+        </footer>
+
+      </section>
+
+
+
+      <!-- LEGACY 语音合成(旧版 *ngIf="false" 死代码,保留作回滚备份) -->
+
+      <section class="section-card" *ngIf="false">
+
+        <div class="search-header">
+
+          <h2>
+
+            <span class="icon">record_voice_over</span> 语音合成
+
+          </h2>
+
+          <p class="subtitle">基于火山引擎API,支持音色复刻与文本语音合成</p>
+
+        </div>
+
+
+
+        <!-- 模式切换 -->
+
+        <div class="remix-mode-grid" style="margin-bottom:20px">
+
+          <div class="remix-mode-card"
+
+               [class.active]="vsTtsMode === 'clone'"
+
+               (click)="setVsTtsMode('clone')">
+
+            <div class="remix-mode-title">音色复刻</div>
+
+            <div class="remix-mode-desc">上传音频URL复刻音色,获取timbreId后可用于语音合成。</div>
+
+          </div>
+
+          <div class="remix-mode-card"
+
+               [class.active]="vsTtsMode === 'synthesize'"
+
+               (click)="setVsTtsMode('synthesize')">
+
+            <div class="remix-mode-title">语音合成</div>
+
+            <div class="remix-mode-desc">使用已有音色ID将文本内容转为语音音频。</div>
+
+          </div>
+
+        </div>
+
+
+
+        <!-- 音色复刻表单 -->
+
+        <div *ngIf="vsTtsMode === 'clone'" style="display:flex;flex-direction:column;gap:14px">
+
+          <div class="form-field">
+
+            <label>音色名称 *</label>
+
+            <input type="text" [(ngModel)]="vsCloneForm.name" placeholder="例如:我的专属音色" [disabled]="vsCloneLoading || vsCloneUploading">
+
+          </div>
+
+          <div class="form-field">
+
+            <label>Speaker ID *</label>
+
+            <select [(ngModel)]="vsCloneForm.speakerId" [disabled]="vsCloneLoading || vsCloneUploading">
+
+              <option value="S_ULZKhZWZ1">S_ULZKhZWZ1</option>
+
+              <option value="S_TLZKhZWZ1">S_TLZKhZWZ1</option>
+
+              <option value="S_SLZKhZWZ1">S_SLZKhZWZ1</option>
+
+              <option value="S_QLZKhZWZ1">S_QLZKhZWZ1</option>
+
+              <option value="S_PLZKhZWZ1">S_PLZKhZWZ1</option>
+
+              <option value="S_OLZKhZWZ1">S_OLZKhZWZ1</option>
+
+              <option value="S_MLZKhZWZ1">S_MLZKhZWZ1</option>
+
+              <option value="S_RWDjSzXZ1">S_RWDjSzXZ1</option>
+
+            </select>
+
+          </div>
+
+          <div class="form-field">
+
+            <label>上传音频文件 *</label>
+
+            <input type="file" id="vsCloneAudioInput"
+
+                   accept=".mp3,.wav,.m4a,.ogg,.aac,.flac,.wma,audio/*"
+
+                   (change)="onVsCloneAudioSelected($event)"
+
+                   style="display:none"
+
+                   [disabled]="vsCloneLoading || vsCloneUploading">
+
+            <div style="border:2px dashed var(--border-color);border-radius:10px;padding:20px;text-align:center;transition:all 0.2s"
+
+                 [style.border-color]="vsCloneAudioName ? 'var(--accent-color)' : 'var(--border-color)'"
+
+                 [style.background]="vsCloneAudioName ? 'var(--accent-color-alpha, rgba(33,150,243,0.05))' : 'transparent'">
+
+              <div *ngIf="!vsCloneAudioName">
+
+                <span class="icon" style="font-size:36px;color:var(--text-secondary);display:block;margin-bottom:6px">audio_file</span>
+
+                <p style="margin:0 0 6px;color:var(--text-primary);font-size:14px;font-weight:500">点击选择音频文件</p>
+
+                <p style="margin:0;color:var(--text-secondary);font-size:12px">支持 MP3、WAV、M4A、OGG、AAC、FLAC 格式,最大 50MB</p>
+
+                <label for="vsCloneAudioInput" class="btn btn-primary" style="margin-top:10px;cursor:pointer;display:inline-flex;font-size:13px">
+
+                  <span class="icon" style="font-size:16px">folder_open</span> 选择音频
+
+                </label>
+
+              </div>
+
+              <div *ngIf="vsCloneAudioName">
+
+                <span class="icon" style="font-size:28px;color:var(--accent-color);display:block;margin-bottom:4px">audio_file</span>
+
+                <p style="margin:0 0 2px;color:var(--text-primary);font-size:14px;font-weight:500">{{vsCloneAudioName}}</p>
+
+                <p *ngIf="vsCloneAudioFile" style="margin:0;color:var(--text-secondary);font-size:12px">
+
+                  文件大小: {{((vsCloneAudioFile?.size || 0) / 1024 / 1024).toFixed(2)}} MB
+
+                </p>
+
+                <p *ngIf="vsCloneAudioUrl" style="margin:4px 0 0;color:var(--success-color);font-size:12px">
+
+                  <span class="icon" style="font-size:14px;vertical-align:middle">check_circle</span> 已上传
+
+                </p>
+
+                <div style="margin-top:8px;display:flex;gap:8px;justify-content:center" *ngIf="!vsCloneLoading && !vsCloneUploading">
+
+                  <label for="vsCloneAudioInput" class="btn" style="cursor:pointer;display:inline-flex;font-size:12px">
+
+                    <span class="icon" style="font-size:14px">swap_horiz</span> 更换文件
+
+                  </label>
+
+                  <button class="btn" (click)="clearVsCloneAudio()" style="font-size:12px">
+
+                    <span class="icon" style="font-size:14px">delete</span> 移除
+
+                  </button>
+
+                </div>
+
+                <p *ngIf="vsCloneUploading" style="margin:6px 0 0;color:var(--accent-color);font-size:12px">
+
+                  <span class="icon" style="font-size:14px;vertical-align:middle">hourglass_empty</span> 正在上传音频...
+
+                </p>
+
+              </div>
+
+            </div>
+
+          </div>
+
+          <div class="form-field">
+
+            <label>音频对应文本(可选)</label>
+
+            <input type="text" [(ngModel)]="vsCloneForm.audioText"
+
+                   placeholder="音频中说的文本内容,有助于提升复刻质量"
+
+                   [disabled]="vsCloneLoading || vsCloneUploading">
+
+          </div>
+
+          <div style="display:flex;gap:12px">
+
+            <div class="form-field" style="flex:1">
+
+              <label>语言</label>
+
+              <select [(ngModel)]="vsCloneForm.language" [disabled]="vsCloneLoading || vsCloneUploading">
+
+                <option [ngValue]="0">中文</option>
+
+                <option [ngValue]="1">英文</option>
+
+              </select>
+
+            </div>
+
+            <div class="form-field" style="flex:2">
+
+              <label>试听文本(4-80字)*</label>
+
+              <input type="text" [(ngModel)]="vsCloneForm.demoText"
+
+                     placeholder="复刻成功后用于生成试听音频的文本"
+
+                     [disabled]="vsCloneLoading || vsCloneUploading">
+
+            </div>
+
+          </div>
+
+
+
+          <div style="color:var(--error-color);font-size:13px" *ngIf="vsCloneError">
+
+            <span class="icon" style="font-size:14px;vertical-align:middle">error</span> {{vsCloneError}}
+
+          </div>
+
+
+
+          <button class="btn btn-primary" (click)="vsStartClone()" [disabled]="vsCloneLoading || vsCloneUploading" style="align-self:flex-start">
+
+            <span class="icon">{{vsCloneLoading || vsCloneUploading ? 'hourglass_empty' : 'content_copy'}}</span>
+
+            {{vsCloneUploading ? '上传音频中...' : vsCloneLoading ? '复刻中...' : '开始音色复刻'}}
+
+          </button>
+
+
+
+          <!-- 复刻结果 -->
+
+          <div *ngIf="vsCloneResult?.code === 200 && vsCloneResult?.data?.timbre"
+
+               style="margin-top:8px;padding:16px;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)">
+
+            <h4 style="margin:0 0 10px;display:flex;align-items:center;gap:6px">
+
+              <span class="icon" style="color:var(--success-color)">check_circle</span> 复刻成功
+
+            </h4>
+
+            <div style="display:flex;flex-direction:column;gap:6px;font-size:13px">
+
+              <div><strong>音色ID(timbreId):</strong><code style="user-select:all">{{vsCloneResult.data.timbre.objectId}}</code></div>
+
+              <div><strong>音色名称:</strong>{{vsCloneResult.data.timbre.name}}</div>
+
+              <div><strong>Speaker ID:</strong>{{vsCloneResult.data.timbre.speaker_id}}</div>
+
+              <div *ngIf="vsCloneResult.data.timbre.models?.length > 0">
+
+                <strong>可用模型:</strong>{{vsCloneResult.data.timbre.models.length}} 个
+
+                <span *ngIf="vsCloneResult.data.timbre.models[0]?.version">({{vsCloneResult.data.timbre.models[0].version}})</span>
+
+              </div>
+
+              <div *ngIf="vsCloneResult.data.timbre.models?.[0]?.demo_audio">
+
+                <strong>试听音频:</strong>
+
+                <audio controls [src]="vsCloneResult.data.timbre.models[0].demo_audio" preload="metadata" style="height:32px;vertical-align:middle"></audio>
+
+              </div>
+
+            </div>
+
+            <button class="btn btn-primary" style="margin-top:12px"
+
+                    (click)="vsSelectClonedTimbre(vsCloneResult.data.timbre.objectId)">
+
+              <span class="icon">arrow_forward</span> 使用此音色进行语音合成
+
+            </button>
+
+          </div>
+
+
+
+          <!-- 已复刻音色快捷列表 -->
+
+          <div *ngIf="vsClonedTimbreList.length > 0" style="margin-top:8px">
+
+            <h4 style="margin:0 0 8px;font-size:14px;color:var(--text-secondary)">已复刻音色</h4>
+
+            <div style="display:flex;flex-wrap:wrap;gap:8px">
+
+              <button class="btn" *ngFor="let t of vsClonedTimbreList"
+
+                      (click)="vsSelectClonedTimbre(t.objectId)"
+
+                      style="font-size:12px">
+
+                <span class="icon" style="font-size:14px">record_voice_over</span>
+
+                {{t.name}} <small style="opacity:0.6">({{t.objectId}})</small>
+
+              </button>
+
+            </div>
+
+          </div>
+
+        </div>
+
+
+
+        <!-- 语音合成表单 -->
+
+        <div *ngIf="vsTtsMode === 'synthesize'" style="display:flex;flex-direction:column;gap:14px">
+
+          <div class="form-field">
+
+            <label>音色ID(timbreId)*</label>
+
+            <input type="text" [(ngModel)]="vsSynthForm.timbreId"
+
+                   placeholder="复刻成功后获得的 objectId,例如:W4WlyF4mKT"
+
+                   [disabled]="vsSynthLoading">
+
+          </div>
+
+
+
+          <!-- 已复刻音色快捷选择 -->
+
+          <div *ngIf="vsClonedTimbreList.length > 0" style="display:flex;flex-wrap:wrap;gap:6px">
+
+            <button class="btn" *ngFor="let t of vsClonedTimbreList"
+
+                    [class.btn-primary]="vsSynthForm.timbreId === t.objectId"
+
+                    (click)="vsSynthForm.timbreId = t.objectId"
+
+                    [disabled]="vsSynthLoading"
+
+                    style="font-size:12px">
+
+              {{t.name}}
+
+            </button>
+
+          </div>
+
+
+
+          <div class="form-field">
+
+            <label>合成文本 *</label>
+
+            <textarea [(ngModel)]="vsSynthForm.text" rows="4"
+
+                      placeholder="请输入要合成的文本内容..."
+
+                      [disabled]="vsSynthLoading"
+
+                      style="resize:vertical;min-height:80px"></textarea>
+
+          </div>
+
+
+
+          <div style="display:flex;gap:12px;flex-wrap:wrap">
+
+            <div class="form-field" style="flex:1;min-width:120px">
+
+              <label>音频格式</label>
+
+              <select [(ngModel)]="vsSynthForm.format" [disabled]="vsSynthLoading">
+
+                <option value="mp3">MP3</option>
+
+                <option value="ogg_opus">OGG Opus</option>
+
+                <option value="pcm">PCM</option>
+
+              </select>
+
+            </div>
+
+            <div class="form-field" style="flex:1;min-width:120px">
+
+              <label>采样率</label>
+
+              <select [(ngModel)]="vsSynthForm.sampleRate" [disabled]="vsSynthLoading">
+
+                <option [ngValue]="8000">8000</option>
+
+                <option [ngValue]="16000">16000</option>
+
+                <option [ngValue]="24000">24000</option>
+
+                <option [ngValue]="48000">48000</option>
+
+              </select>
+
+            </div>
+
+            <div class="form-field" style="flex:1;min-width:120px">
+
+              <label>语速 ({{vsSynthForm.speechRate}})</label>
+
+              <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.speechRate" [disabled]="vsSynthLoading">
+
+            </div>
+
+            <div class="form-field" style="flex:1;min-width:120px">
+
+              <label>音量 ({{vsSynthForm.loudnessRate}})</label>
+
+              <input type="range" min="-50" max="100" [(ngModel)]="vsSynthForm.loudnessRate" [disabled]="vsSynthLoading">
+
+            </div>
+
+          </div>
+
+
+
+          <div style="color:var(--error-color);font-size:13px" *ngIf="vsSynthError">
+
+            <span class="icon" style="font-size:14px;vertical-align:middle">error</span> {{vsSynthError}}
+
+          </div>
+
+
+
+          <button class="btn btn-primary" (click)="vsStartSynthesize()" [disabled]="vsSynthLoading" style="align-self:flex-start">
+
+            <span class="icon">{{vsSynthLoading ? 'hourglass_empty' : 'play_arrow'}}</span>
+
+            {{vsSynthLoading ? '合成中...' : '开始语音合成'}}
+
+          </button>
+
+
+
+          <!-- 合成结果 -->
+
+          <div *ngIf="vsSynthAudioUrl"
+
+               style="margin-top:8px;padding:16px;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color)">
+
+            <h4 style="margin:0 0 10px;display:flex;align-items:center;gap:6px">
+
+              <span class="icon" style="color:var(--success-color)">check_circle</span> 合成成功
+
+            </h4>
+
+            <audio controls [src]="vsSynthAudioUrl" preload="metadata" style="width:100%;margin-bottom:8px"></audio>
+
+            <div style="font-size:13px;display:flex;flex-direction:column;gap:4px">
+
+              <div *ngIf="vsSynthWorkId"><strong>作品ID:</strong><code>{{vsSynthWorkId}}</code></div>
+
+              <div><strong>音频地址:</strong>
+
+                <a [href]="vsSynthAudioUrl" target="_blank" style="color:var(--accent-color);word-break:break-all">{{vsSynthAudioUrl}}</a>
+
+              </div>
+
+            </div>
+
+          </div>
+
+        </div>
+
+
+
+        <!-- 接口说明 -->
+
+        <div style="margin-top:24px;padding:16px;background:var(--bg-tertiary);border-radius:10px;border:1px solid var(--border-color);font-size:13px;color:var(--text-secondary)">
+
+          <h4 style="margin:0 0 8px;color:var(--text-primary)">
+
+            <span class="icon" style="font-size:16px;vertical-align:middle">info</span> 接口说明
+
+          </h4>
+
+          <div style="display:flex;flex-direction:column;gap:4px">
+
+            <div><strong>音色复刻:</strong><code>POST https://server.fmode.cn/api/volcengine/tts/voice_clone</code></div>
+
+            <div><strong>语音合成:</strong><code>POST https://server.fmode.cn/api/volcengine/tts/unidirectional</code></div>
+
+            <div><strong>计费说明:</strong>音色复刻按次计费,语音合成每次 1 credit(0.05元)</div>
+
+          </div>
+
+        </div>
+
+      </section>
+
+
+
+      <!-- 数字人合成模块 -->
+
+      <section class="dh-page" *ngIf="currentTab === 'digital-human'">
+
+        <!-- 顶部标题栏 -->
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
+
+              <line x1="12" y1="5" x2="12" y2="19"></line>
+
+              <line x1="5" y1="12" x2="19" y2="12"></line>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>添加形象</h1>
+
+            <p>上传形象图与音频,一键生成专属数字人口播视频</p>
+
+          </div>
+
+          <button type="button" class="dh-help-btn" (click)="showToast('💡 在「语音合成」标签页复刻音色后,可在此处选择 timbreId 直接生成数字人视频', 'info', 5000)">
+
+            <span class="dh-help-icon">?</span>
+
+            <span>帮助</span>
+
+          </button>
+
+        </header>
+
+
+
+        <!-- ============================================================ -->
+        <!-- 4 栏 Step 流程板:参考视频 → 内容与脚本 → 数字人设置 → 视频预览 -->
+        <!-- ============================================================ -->
+        <div class="dh-board">
+
+          <!-- ===================== Step 1: 参考视频 ===================== -->
+          <article class="dh-step-col">
+            <header class="dh-step-col__head">
+              <span class="dh-step-col__step">Step 1</span>
+              <h3 class="dh-step-col__title">参考视频</h3>
+              <button type="button" class="dh-chip-btn dh-step-col__action"
+                      *ngIf="dhRefVideoUrl || dhRefFile || dhRefTranscript"
+                      (click)="dhRefClear()">清除</button>
+            </header>
+
+            <div class="dh-step-col__body">
+
+              <!-- 未选择文件 -->
+              <div *ngIf="!dhRefFile && !dhRefVideoUrl">
+                <input type="file" id="dhRefVideoInput" accept="video/*" (change)="dhRefSelectFile($event)" hidden>
+                <label for="dhRefVideoInput" class="dh-mini-drop dh-ref-drop">
+                  <span class="dh-mini-drop__title">点击选择参考视频</span>
+                  <span class="dh-mini-drop__hint">支持常见视频格式,建议 ≤ 100MB;上传后将自动开始分析</span>
+                </label>
+                <p class="dh-step-col__hint">上传视频后,AI 会自动识别画面与音频,生成口播文本与提示词。</p>
+              </div>
+
+              <!-- 已选择文件 / 已分析 -->
+              <div *ngIf="dhRefFile || dhRefVideoUrl" class="dh-step-col__stack">
+                <div class="dh-ref-preview-card">
+                  <div class="dh-ref-preview-card__video">
+                    <video *ngIf="dhRefVideoUrl" controls muted [src]="dhRefVideoUrl"></video>
+                  </div>
+                  <div class="dh-ref-preview-card__meta">
+                    <div class="dh-ref-preview-card__name" [title]="dhRefFileName">{{dhRefFileName || '参考视频'}}</div>
+                    <div class="dh-ref-preview-card__size" *ngIf="dhRefFileSize > 0">{{(dhRefFileSize / 1024 / 1024).toFixed(2)}} MB</div>
+                  </div>
+                </div>
+
+                <button type="button" class="dh-cta dh-cta--sm"
+                        *ngIf="dhRefFile && !dhRefUploading && !dhRefAnalyzing && !dhRefVideoId"
+                        (click)="dhRefUploadAndAnalyze()">开始分析</button>
+
+                <!-- 上传 / 分析进度 -->
+                <div class="dh-ref-progress" *ngIf="dhRefUploading">
+                  <span class="dh-spinner"></span>
+                  <span>正在上传...{{dhRefUploadProgress | number:'1.0-0'}}%</span>
+                </div>
+                <div class="dh-ref-progress" *ngIf="dhRefAnalyzing">
+                  <span class="dh-spinner"></span>
+                  <span>{{dhRefAnalysisText || '正在分析视频内容...'}}</span>
+                </div>
+
+                <!-- AI 分析结果(已自动填入 Step 2 口播文本) -->
+                <div class="dh-substep" *ngIf="dhRefTranscript && !dhRefAnalyzing">
+                  <header class="dh-substep__head">
+                    <h4>AI 分析结果</h4>
+                    <span class="dh-chip-btn dh-chip-btn--ghost" style="cursor:default">已同步到口播</span>
+                  </header>
+
+                  <div class="dh-field">
+                    <label class="dh-field__label"><span>视频文字稿(可编辑)</span></label>
+                    <textarea class="dh-textarea" rows="4" [(ngModel)]="dhRefTranscript"
+                              (ngModelChange)="dhRefSyncToTts($event)"></textarea>
+                  </div>
+
+                  <div class="dh-field" *ngIf="dhRefAnalysisText">
+                    <label class="dh-field__label"><span>内容分析</span></label>
+                    <div class="dh-ref-analysis">{{dhRefAnalysisText}}</div>
+                  </div>
+                </div>
+
+                <div class="dh-error" *ngIf="dhRefError">{{dhRefError}}</div>
+              </div>
+
+            </div>
+          </article>
+
+          <!-- ===================== Step 2: 内容与脚本 ===================== -->
+          <article class="dh-step-col">
+            <header class="dh-step-col__head">
+              <span class="dh-step-col__step">Step 2</span>
+              <h3 class="dh-step-col__title">内容与脚本</h3>
+              <span class="dh-step-col__action dh-step-col__counter"
+                    *ngIf="dhTtsText">{{(dhTtsText || '').length}} 字</span>
+            </header>
+
+            <div class="dh-step-col__body dh-step-col__stack">
+
+              <div class="dh-field">
+                <label class="dh-field__label">
+                  <span>口播文本 <em>*</em></span>
+                  <span class="dh-field__counter">{{(dhTtsText || '').length}} / 600</span>
+                </label>
+                <textarea class="dh-textarea" rows="4" maxlength="600" [(ngModel)]="dhTtsText"
+                          placeholder="输入数字人要朗读的文本,将使用所选音色合成语音"></textarea>
+              </div>
+
+              <div class="dh-field">
+                <label class="dh-field__label">
+                  <span>补充提示词(可选)</span>
+                  <span class="dh-field__counter">{{(dhPrompt || '').length}} / 200</span>
+                </label>
+                <textarea class="dh-textarea" rows="3" maxlength="200" [(ngModel)]="dhPrompt"
+                          placeholder="可选:强调产品的易用性,缩短文案至 40 字..."></textarea>
+
+                <!-- 预设风格快捷填入 -->
+                <div class="dh-preset-row">
+                  <button type="button" class="dh-preset-chip"
+                          *ngFor="let p of dhPromptPresets"
+                          [class.is-active]="dhPrompt === p.value"
+                          (click)="dhApplyPromptPreset(p.value)"
+                          [title]="p.value">{{p.label}}</button>
+                </div>
+              </div>
+
+              <!-- AI 风格助手(折叠在内容栏底部) -->
+              <details class="dh-substep dh-chat-fold" [attr.open]="dhChatMessages.length > 0 ? '' : null">
+                <summary class="dh-substep__head dh-chat-fold__summary">
+                  <h4>
+                    <span class="dh-chat-dot"></span>
+                    AI 风格助手
+                    <span class="dh-chat-fold__badge" *ngIf="dhChatMessages.length > 0">{{dhChatMessages.length}}</span>
+                  </h4>
+                  <button type="button" class="dh-chip-btn"
+                          *ngIf="dhChatMessages.length > 0"
+                          (click)="$event.preventDefault(); $event.stopPropagation(); dhChatClear()">清空</button>
+                </summary>
+
+                <p class="dh-chat-fold__desc">用一句话告诉 AI 你想要的语气或风格,它会基于当前口播文本与提示词给出修改建议。</p>
+
+                <div class="dh-chat-quick">
+                  <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
+                          (click)="dhChatQuickAsk('请把口播文本改得更口语化、更接地气,像在和朋友聊天')">更口语化</button>
+                  <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
+                          (click)="dhChatQuickAsk('请把口播文本改得更专业、像新闻播报的语气')">更专业</button>
+                  <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
+                          (click)="dhChatQuickAsk('请把口播文本压缩到 100 字以内,保留核心信息')">压缩到 100 字</button>
+                  <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
+                          (click)="dhChatQuickAsk('在保持原意的前提下,开头加一句吸引人的钩子')">加钩子开头</button>
+                  <button type="button" class="dh-chat-quick__chip" [disabled]="dhChatLoading"
+                          (click)="dhChatQuickAsk('请优化补充提示词,强调自信、亲和的镜头表现')">优化提示词</button>
+                </div>
+
+                <div class="dh-chat-list" *ngIf="dhChatMessages.length > 0">
+                  <ng-container *ngFor="let msg of dhChatMessages; let i = index">
+                    <div class="dh-chat-msg dh-chat-msg--user" *ngIf="msg.role === 'user'">
+                      <div class="dh-chat-bubble dh-chat-bubble--user">{{msg.content}}</div>
+                    </div>
+                    <div class="dh-chat-msg dh-chat-msg--ai" *ngIf="msg.role === 'assistant'">
+                      <div class="dh-chat-avatar">AI</div>
+                      <div class="dh-chat-bubble dh-chat-bubble--ai">
+                        <div class="dh-chat-bubble__text">{{msg.content}}</div>
+                        <div class="dh-chat-suggest" *ngIf="msg.suggestedTts">
+                          <div class="dh-chat-suggest__label">建议口播文本</div>
+                          <div class="dh-chat-suggest__text">{{msg.suggestedTts}}</div>
+                        </div>
+                        <div class="dh-chat-suggest" *ngIf="msg.suggestedPrompt">
+                          <div class="dh-chat-suggest__label">建议补充提示词</div>
+                          <div class="dh-chat-suggest__text">{{msg.suggestedPrompt}}</div>
+                        </div>
+                        <div class="dh-chat-actions" *ngIf="msg.suggestedTts || msg.suggestedPrompt">
+                          <button type="button" class="dh-chat-apply" *ngIf="msg.suggestedTts"
+                                  [disabled]="msg.appliedTts" (click)="dhChatApply(i, 'tts')">
+                            {{msg.appliedTts ? '✓ 口播已应用' : '应用到口播'}}
+                          </button>
+                          <button type="button" class="dh-chat-apply" *ngIf="msg.suggestedPrompt"
+                                  [disabled]="msg.appliedPrompt" (click)="dhChatApply(i, 'prompt')">
+                            {{msg.appliedPrompt ? '✓ 提示词已应用' : '应用到提示词'}}
+                          </button>
+                          <button type="button" class="dh-chat-apply dh-chat-apply--primary"
+                                  *ngIf="msg.suggestedTts && msg.suggestedPrompt"
+                                  [disabled]="msg.appliedTts && msg.appliedPrompt"
+                                  (click)="dhChatApply(i, 'both')">全部应用</button>
+                        </div>
+                      </div>
+                    </div>
+                  </ng-container>
+                  <div class="dh-chat-msg dh-chat-msg--ai" *ngIf="dhChatLoading">
+                    <div class="dh-chat-avatar">AI</div>
+                    <div class="dh-chat-bubble dh-chat-bubble--ai">
+                      <span class="dh-spinner"></span>
+                      <span style="margin-left:8px">AI 正在思考...</span>
+                    </div>
+                  </div>
+                </div>
+
+                <div class="dh-chat-inputbar">
+                  <textarea class="dh-textarea dh-chat-textarea" rows="2"
+                            [(ngModel)]="dhChatInput" [disabled]="dhChatLoading"
+                            placeholder="例如:把语气改得更轻松一点、加一个反问句..."
+                            (keydown.enter)="!$any($event).shiftKey && ($any($event).preventDefault() || dhChatSend())"></textarea>
+                  <button type="button" class="dh-cta dh-cta--sm dh-chat-send"
+                          [disabled]="dhChatLoading || !dhChatInput.trim()"
+                          (click)="dhChatSend()">
+                    <span *ngIf="!dhChatLoading">发送</span>
+                    <span *ngIf="dhChatLoading" class="dh-cta__spinner-row">
+                      <span class="dh-spinner"></span><span>发送中</span>
+                    </span>
+                  </button>
+                </div>
+                <div class="dh-error" *ngIf="dhChatError">{{dhChatError}}</div>
+              </details>
+
+            </div>
+          </article>
+
+          <!-- ===================== Step 3: 数字人设置 ===================== -->
+          <article class="dh-step-col">
+            <header class="dh-step-col__head">
+              <span class="dh-step-col__step">Step 3</span>
+              <h3 class="dh-step-col__title">数字人设置</h3>
+            </header>
+
+            <div class="dh-step-col__body dh-step-col__stack">
+
+              <!-- 形象图上传 -->
+              <div class="dh-substep">
+                <header class="dh-substep__head">
+                  <h4>形象图上传 <em>*</em></h4>
+                </header>
+                <input type="file" id="dhImageInput"
+                       accept="image/jpeg,image/png,image/jpg,image/gif,.jpg,.jpeg,.png,.gif"
+                       (change)="onDhImageSelected($event)" hidden [disabled]="dhLoading">
+
+                <label for="dhImageInput" class="dh-image-card dh-image-card--empty" *ngIf="!dhImagePreviewUrl">
+                  <svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
+                    <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
+                  </svg>
+                  <span class="dh-image-card__title">点击上传形象图</span>
+                  <ul class="dh-image-card__hints">
+                    <li>支持 JPG / PNG / GIF</li>
+                    <li>建议清晰单人正面照</li>
+                    <li>最大 20MB</li>
+                  </ul>
+                </label>
+
+                <div class="dh-image-card dh-image-card--filled" *ngIf="dhImagePreviewUrl">
+                  <img [src]="dhImagePreviewUrl" [alt]="dhImageName || '数字人形象图'">
+                  <div class="dh-image-card__meta">
+                    <div class="dh-image-card__name">{{dhImageName || '未命名形象'}}</div>
+                    <div class="dh-image-card__size" *ngIf="dhImageFile">{{(dhImageFile.size / 1024 / 1024).toFixed(2)}} MB</div>
+                    <div class="dh-image-card__actions">
+                      <label for="dhImageInput" class="dh-chip-btn">更换</label>
+                      <button type="button" class="dh-chip-btn dh-chip-btn--danger" (click)="clearDhImage()">移除</button>
+                    </div>
+                  </div>
+                </div>
+
+                <div class="dh-field" style="margin-top:10px">
+                  <label class="dh-field__label">
+                    <span>形象名称</span>
+                    <span class="dh-field__counter">{{(dhImageName || '').length}} / 60</span>
+                  </label>
+                  <input class="dh-input" type="text" [(ngModel)]="dhImageName" maxlength="60" placeholder="请输入数字人名称">
+                </div>
+              </div>
+
+              <!-- 音频来源 -->
+              <div class="dh-substep">
+                <header class="dh-substep__head">
+                  <h4>音频来源 <em>*</em></h4>
+                </header>
+                <div class="dh-segment">
+                  <button type="button" class="dh-segment__item" [class.is-active]="dhAudioSource === 'tts'" (click)="dhAudioSource = 'tts'; dhError = ''">语音合成</button>
+                  <button type="button" class="dh-segment__item" [class.is-active]="dhAudioSource === 'upload'" (click)="dhAudioSource = 'upload'; dhError = ''">提供音频</button>
+                </div>
+
+                <ng-container *ngIf="dhAudioSource === 'tts'">
+                  <div class="dh-field" style="margin-top:10px">
+                    <label class="dh-field__label"><span>音色 ID(timbreId)<em>*</em></span></label>
+                    <input class="dh-input" type="text" [(ngModel)]="dhTtsTimbreId" placeholder="在「语音合成」标签页复刻音色后获得 objectId">
+                  </div>
+                  <div class="dh-voice-list" *ngIf="vsClonedTimbreList.length > 0">
+                    <button type="button" class="dh-voice-card" *ngFor="let t of vsClonedTimbreList"
+                            [class.is-active]="dhTtsTimbreId === t.objectId"
+                            (click)="dhTtsTimbreId = t.objectId">
+                      <span class="dh-voice-card__dot"></span>
+                      <span class="dh-voice-card__name">{{t.name}}</span>
+                      <span class="dh-voice-card__check">✓</span>
+                    </button>
+                  </div>
+                  <p class="dh-step-col__hint" *ngIf="vsClonedTimbreList.length === 0">尚无复刻音色,可前往「语音合成」标签页创建。</p>
+                </ng-container>
+
+                <ng-container *ngIf="dhAudioSource === 'upload'">
+                  <div class="dh-field" style="margin-top:10px">
+                    <label class="dh-field__label"><span>音频文件 <em>*</em></span></label>
+                    <input type="file" id="dhAudioInput" accept=".mp3,.wav,.m4a,.ogg,.aac,.flac,.wma,audio/*" (change)="onDhAudioSelected($event)" hidden>
+                    <label for="dhAudioInput" class="dh-mini-drop" *ngIf="!dhUploadAudioName">
+                      <span class="dh-mini-drop__title">点击选择音频文件</span>
+                      <span class="dh-mini-drop__hint">支持 MP3 / WAV / M4A / OGG / AAC / FLAC,最大 50MB</span>
+                    </label>
+                    <div class="dh-mini-drop dh-mini-drop--filled" *ngIf="dhUploadAudioName">
+                      <div>
+                        <div class="dh-mini-drop__name">{{dhUploadAudioName}}</div>
+                        <div class="dh-mini-drop__sub" *ngIf="dhUploadAudioFile">{{(dhUploadAudioFile.size / 1024 / 1024).toFixed(2)}} MB</div>
+                      </div>
+                      <div class="dh-mini-drop__actions">
+                        <label for="dhAudioInput" class="dh-chip-btn">更换</label>
+                        <button type="button" class="dh-chip-btn dh-chip-btn--danger" (click)="clearDhAudio()">移除</button>
+                      </div>
+                    </div>
+                  </div>
+                </ng-container>
+              </div>
+
+              <!-- 输出设置 -->
+              <div class="dh-substep">
+                <header class="dh-substep__head">
+                  <h4>输出设置</h4>
+                </header>
+
+                <div class="dh-field">
+                  <label class="dh-field__label"><span>分辨率</span></label>
+                  <div class="dh-segment">
+                    <button type="button" class="dh-segment__item" [class.is-active]="dhResolution === '720p'" (click)="dhResolution = '720p'">720P</button>
+                    <button type="button" class="dh-segment__item" [class.is-active]="dhResolution === '1080p'" (click)="dhResolution = '1080p'">1080P</button>
+                  </div>
+                </div>
+
+                <label class="dh-toggle" style="margin-top:10px">
+                  <input type="checkbox" [(ngModel)]="dhFastMode">
+                  <span class="dh-toggle__track"><span class="dh-toggle__thumb"></span></span>
+                  <span class="dh-toggle__label">快速模式<small>720P 自动启用</small></span>
+                </label>
+              </div>
+
+            </div>
+          </article>
+
+          <!-- ===================== Step 4: 视频预览 ===================== -->
+          <article class="dh-step-col dh-step-col--preview">
+            <header class="dh-step-col__head">
+              <span class="dh-step-col__step">Step 4</span>
+              <h3 class="dh-step-col__title">视频预览</h3>
+            </header>
+
+            <div class="dh-step-col__body dh-step-col__stack">
+
+              <!-- 预览主区 -->
+              <div class="dh-preview-stage">
+                <!-- 空状态 -->
+                <div class="dh-preview-stage__empty" *ngIf="dhStep === 'form' && !dhResultVideoUrl">
+                  <div class="dh-preview-stage__icon">
+                    <svg viewBox="0 0 24 24" width="36" height="36" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
+                      <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
+                    </svg>
+                  </div>
+                  <p class="dh-preview-stage__title">完成以上步骤后</p>
+                  <p class="dh-preview-stage__sub">点击下方按钮即可生成数字人视频</p>
+                </div>
+
+                <!-- 生成中 -->
+                <div class="dh-preview-stage__loading" *ngIf="dhStep === 'generating'">
+                  <span class="dh-spinner dh-spinner--lg"></span>
+                  <p class="dh-preview-stage__title">{{dhStatusText || '正在生成...'}}</p>
+                  <div class="dh-progress-bar"><div class="dh-progress-bar__fill" [style.width]="dhProgress + '%'"></div></div>
+                  <p class="dh-preview-stage__sub">{{dhProgress | number:'1.0-0'}}%</p>
+                </div>
+
+                <!-- 完成 -->
+                <div class="dh-preview-stage__done" *ngIf="dhStep === 'done' && dhResultVideoUrl">
+                  <video controls [src]="dhResultVideoUrl"></video>
+                </div>
+
+                <!-- 失败 -->
+                <div class="dh-preview-stage__error" *ngIf="dhStep === 'done' && !dhResultVideoUrl && dhError">
+                  <span class="dh-preview-stage__cross">!</span>
+                  <p class="dh-preview-stage__title">生成失败</p>
+                  <p class="dh-preview-stage__sub">{{dhError}}</p>
+                </div>
+              </div>
+
+              <!-- 主 CTA -->
+              <button type="button" class="dh-cta dh-cta--xl"
+                      *ngIf="dhStep !== 'done' || !dhResultVideoUrl"
+                      (click)="dhStartGeneration()"
+                      [disabled]="dhLoading || dhStep === 'generating'">
+                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                  <polygon points="5 3 19 12 5 21 5 3"></polygon>
+                </svg>
+                <span>{{dhStep === 'generating' ? '生成中...' : '立即生成'}}</span>
+              </button>
+
+              <div class="dh-error" *ngIf="dhError && dhStep !== 'done'">{{dhError}}</div>
+
+              <!-- 步骤状态清单 -->
+              <ul class="dh-checklist">
+                <li class="dh-checklist__item" [class.is-done]="!!dhRefTranscript">
+                  <span class="dh-checklist__dot"></span>
+                  <span class="dh-checklist__label">分析参考视频</span>
+                  <span class="dh-checklist__status">{{dhRefTranscript ? '完成' : (dhRefAnalyzing ? '进行中' : '待处理')}}</span>
+                </li>
+                <li class="dh-checklist__item" [class.is-done]="!!(dhTtsText && dhTtsText.trim())">
+                  <span class="dh-checklist__dot"></span>
+                  <span class="dh-checklist__label">提取口播内容</span>
+                  <span class="dh-checklist__status">{{dhTtsText && dhTtsText.trim() ? '完成' : '待处理'}}</span>
+                </li>
+                <li class="dh-checklist__item"
+                    [class.is-done]="dhProgress >= 25 && dhStep !== 'form'"
+                    [class.is-active]="dhSynthesizing || (dhAudioSource === 'upload' && dhUploadingAudio)">
+                  <span class="dh-checklist__dot"></span>
+                  <span class="dh-checklist__label">音频合成中</span>
+                  <span class="dh-checklist__status">{{(dhSynthesizing || dhUploadingAudio) ? '进行中' : (dhProgress >= 25 ? '完成' : '待处理')}}</span>
+                </li>
+                <li class="dh-checklist__item"
+                    [class.is-done]="dhStep === 'done' && !!dhResultVideoUrl"
+                    [class.is-active]="dhStep === 'generating' && dhProgress >= 50">
+                  <span class="dh-checklist__dot"></span>
+                  <span class="dh-checklist__label">渲染数字人</span>
+                  <span class="dh-checklist__status">{{dhStep === 'done' && dhResultVideoUrl ? '完成' : (dhStep === 'generating' && dhProgress >= 50 ? '进行中' : '待处理')}}</span>
+                </li>
+                <li class="dh-checklist__item" [class.is-done]="dhStep === 'done' && !!dhResultVideoUrl">
+                  <span class="dh-checklist__dot"></span>
+                  <span class="dh-checklist__label">视频生成导出</span>
+                  <span class="dh-checklist__status">{{dhStep === 'done' && dhResultVideoUrl ? '完成' : '待处理'}}</span>
+                </li>
+              </ul>
+
+              <!-- 完成后的操作栏 -->
+              <div class="dh-preview-actions" *ngIf="dhStep === 'done' && dhResultVideoUrl">
+                <a [href]="dhResultVideoUrl" download class="dh-preview-action">
+                  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+                    <polyline points="7 10 12 15 17 10"></polyline>
+                    <line x1="12" y1="15" x2="12" y2="3"></line>
+                  </svg>
+                  <span>下载视频</span>
+                </a>
+                <a [href]="dhResultVideoUrl" target="_blank" class="dh-preview-action">
+                  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
+                    <polyline points="15 3 21 3 21 9"></polyline>
+                    <line x1="10" y1="14" x2="21" y2="3"></line>
+                  </svg>
+                  <span>新窗口播放</span>
+                </a>
+                <button type="button" class="dh-preview-action" (click)="dhReset()">
+                  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                    <polyline points="23 4 23 10 17 10"></polyline>
+                    <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
+                  </svg>
+                  <span>重新生成</span>
+                </button>
+              </div>
+
+            </div>
+          </article>
+
+        </div>
+
+
+
+
+        <!-- 我的形象库 -->
+
+        <section class="dh-gallery">
+
+          <header class="dh-gallery__header">
+
+            <div>
+
+              <h2>我的形象库</h2>
+
+              <p>查看您创建的所有数字人作品</p>
+
+            </div>
+
+            <button type="button" class="dh-refresh-btn" (click)="refreshDigitalHumanGallery()">
+
+              <span class="dh-refresh-icon">↻</span>
+
+              <span>刷新</span>
+
+            </button>
+
+          </header>
+
+
+
+          <div class="dh-gallery__empty" *ngIf="!getDigitalHumanResults().length">
+
+            <div class="dh-gallery__empty-icon">🎬</div>
+
+            <p>还没有数字人作品,先在上方创建一个吧</p>
+
+          </div>
+
+
+
+          <div class="dh-gallery__grid" *ngIf="getDigitalHumanResults().length">
+
+            <article class="dh-gallery__card" *ngFor="let r of getDigitalHumanResults()">
+
+              <div class="dh-gallery__media">
+
+                <video [src]="r.url" preload="metadata" muted playsinline></video>
+
+                <a [href]="r.url" target="_blank" class="dh-gallery__play" title="播放">▶</a>
+
+              </div>
+
+              <div class="dh-gallery__body">
+
+                <div class="dh-gallery__title-row">
+
+                  <h4 class="dh-gallery__title" [title]="r.title">{{r.title}}</h4>
+
+                  <div class="dh-gallery__icons">
+
+                    <a [href]="r.url" download class="dh-gallery__icon-btn" title="下载">⬇</a>
+
+                    <button type="button" class="dh-gallery__icon-btn dh-gallery__icon-btn--danger" (click)="removeResult(r.id)" title="删除">🗑</button>
+
+                  </div>
+
+                </div>
+
+                <div class="dh-gallery__info">
+
+                  <span>{{r.quality || '—'}}</span>
+
+                  <span>{{r.created_at | date:'yyyy/M/d HH:mm:ss'}}</span>
+
+                  <span class="dh-gallery__chip">MP4</span>
+
+                </div>
+
+              </div>
+
+            </article>
+
+          </div>
+
+        </section>
+
+      </section>
+
+
+
+
+
+      <!-- 视频生成工作流模块 -->
+
+      <section class="dh-page vg-page" *ngIf="currentTab === 'video-generation'">
+
+        <header class="dh-page-header">
+
+          <div class="dh-header-icon">
+
+            <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+
+              <polygon points="23 7 16 12 23 17 23 7"></polygon>
+
+              <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect>
+
+            </svg>
+
+          </div>
+
+          <div class="dh-header-text">
+
+            <h1>视频生成工作流</h1>
+
+            <p>上传视频 → 拆解分析 → 生成脚本 → AI 配图 → 语音配音 → 视频合成</p>
+
+          </div>
+
+        </header>
+
+
+
+        <!-- 步骤进度条 -->
+
+        <div class="vg-step-bar">
+
+          <ng-container *ngFor="let label of vgStepLabels; let i = index">
+
+            <div class="vg-step-item"
+
+                 [class.is-active]="vgStep === i + 1"
+
+                 [class.is-done]="vgStep > i + 1">
+
+              <div class="vg-step-badge">{{vgStep > i + 1 ? '✓' : i + 1}}</div>
+
+              <span class="vg-step-label">{{label}}</span>
+
+            </div>
+
+            <div *ngIf="i < vgStepLabels.length - 1"
+
+                 class="vg-step-connector"
+
+                 [class.is-done]="vgStep > i + 1"></div>
+
+          </ng-container>
+
+        </div>
+
+
+
+        <!-- 错误提示 -->
+
+        <div *ngIf="vgError" style="margin-bottom:16px;padding:12px 16px;background:rgba(244,67,54,0.08);border-radius:8px;color:var(--error-color);font-size:13px;display:flex;align-items:center;gap:8px">
+
+          <span class="icon" style="font-size:16px">error</span>
+
+          {{vgError}}
+
+          <button class="btn btn-sm" style="margin-left:auto" (click)="vgError = ''">
+
+            <span class="icon" style="font-size:14px">close</span>
+
+          </button>
+
+        </div>
+
+
+
+        <!-- ===== Step 1: 上传视频 ===== -->
+
+        <section class="vg-step-card" [attr.data-state]="vgGetStepState(1)" [class.is-collapsed]="!vgIsStepExpanded(1)">
+
+          <header class="vg-step-card__header" (click)="vgToggleStepExpand(1)">
+
+            <div class="vg-step-card__num">{{vgStep > 1 ? '✓' : '1'}}</div>
+
+            <div class="vg-step-card__meta">
+
+              <h3>{{vgStepLabels[0]}}</h3>
+
+              <p>{{vgStepSubtitles[0]}}</p>
+
+            </div>
+
+            <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(1)">{{ vgGetStepState(1) === 'done' ? '已完成' : vgGetStepState(1) === 'active' ? '进行中' : '待解锁' }}</div>
+
+            <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(1) === 'done'" (click)="$event.stopPropagation(); vgEditStep(1)" title="返回此步骤编辑">编辑</button>
+
+            <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(1) !== 'done'" tabindex="-1" aria-label="展开/折叠">
+
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
+
+            </button>
+
+          </header>
+
+          <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(1)">
+
+
+
+          <div style="border:2px dashed var(--border-color);border-radius:12px;padding:40px;text-align:center;background:var(--bg-secondary);position:relative"
+
+               [style.borderColor]="vgSourceFile ? 'var(--accent-color)' : 'var(--border-color)'">
+
+            <input type="file" accept="video/*" (change)="vgSelectFile($event)"
+
+                   style="position:absolute;inset:0;opacity:0;cursor:pointer">
+
+            <div *ngIf="!vgSourceFile">
+
+              <svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
+
+                   style="display:block;margin:0 auto 12px;color:var(--text-secondary)">
+
+                <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
+
+                <polyline points="17 8 12 3 7 8"></polyline>
+
+                <line x1="12" y1="3" x2="12" y2="15"></line>
+
+              </svg>
+
+              <p style="margin:0;font-size:15px;color:var(--text-primary)">点击或拖拽上传视频文件</p>
+
+              <p style="margin:6px 0 0;font-size:12px;color:var(--text-secondary)">支持 MP4、MOV、AVI、WebM 等格式</p>
+
+            </div>
+
+            <div *ngIf="vgSourceFile" style="display:flex;align-items:center;gap:16px;justify-content:center">
+
+              <span class="icon" style="font-size:36px;color:var(--accent-color)">movie</span>
+
+              <div style="text-align:left">
+
+                <p style="margin:0;font-size:14px;font-weight:500">{{vgSourceFileName}}</p>
+
+                <p style="margin:4px 0 0;font-size:12px;color:var(--text-secondary)">
+
+                  {{(vgSourceFile.size / 1024 / 1024).toFixed(1)}} MB
+
+                </p>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 视频预览 -->
+
+          <div *ngIf="vgSourceVideoUrl" style="margin-top:16px;text-align:center">
+
+            <video [src]="vgSourceVideoUrl" controls
+
+                   style="max-width:100%;max-height:300px;border-radius:8px;background:#000"></video>
+
+          </div>
+
+
+
+          <!-- 上传进度 -->
+
+          <div *ngIf="vgUploading" style="margin-top:16px">
+
+            <div style="display:flex;justify-content:space-between;margin-bottom:6px">
+
+              <span style="font-size:13px">正在上传...</span>
+
+              <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{vgUploadProgress}}%</span>
+
+            </div>
+
+            <div class="progress-bar" style="height:6px">
+
+              <div class="progress-fill" [style.width]="vgUploadProgress + '%'"></div>
+
+            </div>
+
+          </div>
+
+
+
+          <div style="margin-top:20px;display:flex;justify-content:flex-end">
+
+            <button class="btn btn-primary" (click)="vgUploadAndAnalyze()"
+
+                    [disabled]="!vgSourceFile || vgUploading">
+
+              <span class="icon">{{vgUploading ? 'hourglass_empty' : 'upload'}}</span>
+
+              {{vgUploading ? '上传中...' : '上传并开始分析'}}
+
+            </button>
+
+          </div>
+
+          </div>
+
+        </section>
+
+
+
+        <!-- ===== Step 2: 视频解析 ===== -->
+
+        <section class="vg-step-card" [attr.data-state]="vgGetStepState(2)" [class.is-collapsed]="!vgIsStepExpanded(2)">
+
+          <header class="vg-step-card__header" (click)="vgToggleStepExpand(2)">
+
+            <div class="vg-step-card__num">{{vgStep > 2 ? '✓' : '2'}}</div>
+
+            <div class="vg-step-card__meta">
+
+              <h3>{{vgStepLabels[1]}}</h3>
+
+              <p>{{vgStepSubtitles[1]}}</p>
+
+            </div>
+
+            <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(2)">{{ vgGetStepState(2) === 'done' ? '已完成' : vgGetStepState(2) === 'active' ? '进行中' : '待解锁' }}</div>
+
+            <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(2) === 'done'" (click)="$event.stopPropagation(); vgEditStep(2)" title="返回此步骤编辑">编辑</button>
+
+            <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(2) !== 'done'" tabindex="-1" aria-label="展开/折叠">
+
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
+
+            </button>
+
+          </header>
+
+          <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(2)">
+
+
+
+          <!-- 解析状态 -->
+
+          <div *ngIf="vgAnalyzing" style="padding:24px;text-align:center;background:var(--bg-secondary);border-radius:10px;margin-bottom:16px">
+
+            <div class="spinner" style="margin:0 auto 12px"></div>
+
+            <p style="margin:0;color:var(--text-primary)">{{vgAnalysisText}}</p>
+
+          </div>
+
+
+
+          <!-- AI分析结果 -->
+
+          <div *ngIf="!vgAnalyzing && vgAnalysisText" style="padding:16px;background:var(--bg-secondary);border-radius:10px;margin-bottom:16px;border:1px solid var(--border-color)">
+
+            <h4 style="margin:0 0 10px;display:flex;align-items:center;gap:6px">
+
+              <span class="icon" style="color:var(--accent-color)">smart_toy</span> AI 内容分析
+
+            </h4>
+
+            <pre style="margin:0;white-space:pre-wrap;font-size:13px;line-height:1.6;color:var(--text-primary);font-family:inherit">{{vgAnalysisText}}</pre>
+
+          </div>
+
+
+
+          <!-- 转录文字稿 -->
+
+          <div style="margin-bottom:16px">
+
+            <label style="display:block;font-size:13px;font-weight:500;margin-bottom:6px">
+
+              <span class="icon" style="font-size:14px;vertical-align:middle">description</span> 转录文字稿
+
+            </label>
+
+            <textarea [(ngModel)]="vgTranscript" rows="8"
+
+                      placeholder="视频转录文字稿将在此显示,也可手动输入或修改..."
+
+                      style="width:100%;resize:vertical;min-height:120px;border:1px solid var(--border-color);border-radius:8px;padding:12px;font-size:13px;line-height:1.6;background:var(--bg-primary);color:var(--text-primary)"></textarea>
+
+          </div>
+
+
+
+          <div style="display:flex;justify-content:space-between">
+
+            <button class="btn" (click)="vgPrevStep()">
+
+              <span class="icon">arrow_back</span> 上一步
+
+            </button>
+
+            <button class="btn btn-primary" (click)="vgConfirmAnalysis()"
+
+                    [disabled]="!vgTranscript.trim() || vgAnalyzing">
+
+              <span class="icon">arrow_forward</span> 确认并生成脚本
+
+            </button>
+
+          </div>
+
+          </div>
+
+        </section>
+
+
+
+        <!-- ===== Step 3: 脚本生成 ===== -->
+
+        <section class="vg-step-card" [attr.data-state]="vgGetStepState(3)" [class.is-collapsed]="!vgIsStepExpanded(3)">
+
+          <header class="vg-step-card__header" (click)="vgToggleStepExpand(3)">
+
+            <div class="vg-step-card__num">{{vgStep > 3 ? '✓' : '3'}}</div>
+
+            <div class="vg-step-card__meta">
+
+              <h3>{{vgStepLabels[2]}}</h3>
+
+              <p>{{vgStepSubtitles[2]}}</p>
+
+            </div>
+
+            <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(3)">{{ vgGetStepState(3) === 'done' ? '已完成' : vgGetStepState(3) === 'active' ? '进行中' : '待解锁' }}</div>
+
+            <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(3) === 'done'" (click)="$event.stopPropagation(); vgEditStep(3)" title="返回此步骤编辑">编辑</button>
+
+            <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(3) !== 'done'" tabindex="-1" aria-label="展开/折叠">
+
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
+
+            </button>
+
+          </header>
+
+          <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(3)">
+
+
+
+          <div *ngIf="vgGeneratingScript" style="padding:24px;text-align:center;background:var(--bg-secondary);border-radius:10px;margin-bottom:16px">
+
+            <div class="spinner" style="margin:0 auto 12px"></div>
+
+            <p style="margin:0;color:var(--text-primary)">AI 正在生成分镜脚本...</p>
+
+          </div>
+
+
+
+          <!-- 脚本片段列表 -->
+
+          <div *ngIf="!vgGeneratingScript && vgScriptSegments.length > 0" style="display:flex;flex-direction:column;gap:12px;margin-bottom:16px">
+
+            <div *ngFor="let seg of vgScriptSegments; let i = index"
+
+                 style="padding:14px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+              <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
+
+                <span style="background:var(--accent-color);color:#fff;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600">
+
+                  {{seg.id}}
+
+                </span>
+
+              </div>
+
+              <div style="margin-bottom:8px">
+
+                <label style="font-size:11px;color:var(--text-secondary);display:block;margin-bottom:4px">旁白文本</label>
+
+                <textarea [(ngModel)]="seg.narration" rows="2"
+
+                          style="width:100%;resize:vertical;border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:13px;background:var(--bg-primary);color:var(--text-primary)"></textarea>
+
+              </div>
+
+              <div>
+
+                <label style="font-size:11px;color:var(--text-secondary);display:block;margin-bottom:4px">配图提示词(英文,用于AI绘画)</label>
+
+                <textarea [(ngModel)]="seg.imagePrompt" rows="2"
+
+                          style="width:100%;resize:vertical;border:1px solid var(--border-color);border-radius:6px;padding:8px;font-size:12px;font-family:monospace;background:var(--bg-primary);color:var(--text-primary)"></textarea>
+
+              </div>
+
+              <div *ngIf="seg.imagePromptCn" style="margin-top:6px">
+
+                <label style="font-size:11px;color:var(--text-secondary);display:block;margin-bottom:4px">配图中文说明</label>
+
+                <div style="padding:8px;background:var(--bg-primary);border:1px solid var(--border-color);border-radius:6px;font-size:12px;color:var(--text-secondary)">{{seg.imagePromptCn}}</div>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
+
+            <button class="btn" (click)="vgPrevStep()">
+
+              <span class="icon">arrow_back</span> 上一步
+
+            </button>
+
+            <div style="display:flex;gap:8px">
+
+              <button class="btn" (click)="vgRegenerateScript()" [disabled]="vgGeneratingScript">
+
+                <span class="icon">refresh</span> 重新生成
+
+              </button>
+
+              <button class="btn btn-primary" (click)="vgConfirmScript()"
+
+                      [disabled]="vgScriptSegments.length === 0 || vgGeneratingScript">
+
+                <span class="icon">arrow_forward</span> 确认并配图
+
+              </button>
+
+            </div>
+
+          </div>
+
+          </div>
+
+        </section>
+
+
+
+        <!-- ===== Step 4: 配图生成 ===== -->
+
+        <section class="vg-step-card" [attr.data-state]="vgGetStepState(4)" [class.is-collapsed]="!vgIsStepExpanded(4)">
+
+          <header class="vg-step-card__header" (click)="vgToggleStepExpand(4)">
+
+            <div class="vg-step-card__num">{{vgStep > 4 ? '✓' : '4'}}</div>
+
+            <div class="vg-step-card__meta">
+
+              <h3>{{vgStepLabels[3]}}</h3>
+
+              <p>{{vgStepSubtitles[3]}}</p>
+
+            </div>
+
+            <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(4)">{{ vgGetStepState(4) === 'done' ? '已完成' : vgGetStepState(4) === 'active' ? '进行中' : '待解锁' }}</div>
+
+            <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(4) === 'done'" (click)="$event.stopPropagation(); vgEditStep(4)" title="返回此步骤编辑">编辑</button>
+
+            <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(4) !== 'done'" tabindex="-1" aria-label="展开/折叠">
+
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
+
+            </button>
+
+          </header>
+
+          <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(4)">
+
+
+
+          <!-- 配图进度 -->
+
+          <div *ngIf="vgGeneratingImages" style="margin-bottom:16px">
+
+            <div style="display:flex;justify-content:space-between;margin-bottom:6px">
+
+              <span style="font-size:13px">正在生成配图...</span>
+
+              <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{vgImageProgress}}%</span>
+
+            </div>
+
+            <div class="progress-bar" style="height:6px">
+
+              <div class="progress-fill" [style.width]="vgImageProgress + '%'"></div>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 图片网格 -->
+
+          <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;margin-bottom:16px">
+
+            <div *ngFor="let img of vgImageResults"
+
+                 style="background:var(--bg-secondary);border-radius:10px;overflow:hidden;border:1px solid var(--border-color)">
+
+              <div style="aspect-ratio:16/9;display:flex;align-items:center;justify-content:center;background:var(--bg-tertiary)">
+
+                <img *ngIf="img.imageUrl" [src]="img.imageUrl" style="width:100%;height:100%;object-fit:cover">
+
+                <div *ngIf="!img.imageUrl && img.status === 'generating'" class="spinner"></div>
+
+                <span *ngIf="!img.imageUrl && img.status === 'pending'" class="icon" style="font-size:32px;color:var(--text-secondary)">image</span>
+
+                <span *ngIf="!img.imageUrl && img.status === 'failed'" class="icon" style="font-size:32px;color:var(--error-color)">broken_image</span>
+
+              </div>
+
+              <div style="padding:8px">
+
+                <span style="font-size:12px;font-weight:600">{{img.id}}</span>
+
+                <span style="font-size:11px;margin-left:6px;padding:2px 6px;border-radius:4px"
+
+                      [style.background]="img.status === 'done' ? 'rgba(76,175,80,0.15)' : img.status === 'failed' ? 'rgba(244,67,54,0.15)' : img.status === 'generating' ? 'rgba(33,150,243,0.15)' : 'var(--bg-tertiary)'"
+
+                      [style.color]="img.status === 'done' ? 'var(--success-color)' : img.status === 'failed' ? 'var(--error-color)' : 'var(--text-secondary)'">
+
+                  {{img.status === 'done' ? '完成' : img.status === 'failed' ? '失败' : img.status === 'generating' ? '生成中' : '待生成'}}
+
+                </span>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
+
+            <button class="btn" (click)="vgPrevStep()">
+
+              <span class="icon">arrow_back</span> 上一步
+
+            </button>
+
+            <div style="display:flex;gap:8px">
+
+              <button class="btn btn-primary" (click)="vgGenerateImages()"
+
+                      [disabled]="vgGeneratingImages"
+
+                      *ngIf="vgImageResults.some(r => r.status === 'pending')">
+
+                <span class="icon">auto_fix_high</span>
+
+                {{vgGeneratingImages ? '生成中...' : '开始生成配图'}}
+
+              </button>
+
+              <button class="btn btn-primary" (click)="vgConfirmImages()"
+
+                      [disabled]="vgGeneratingImages">
+
+                <span class="icon">arrow_forward</span> 下一步:配音
+
+              </button>
+
+            </div>
+
+          </div>
+
+          </div>
+
+        </section>
+
+
+
+        <!-- ===== Step 5: 语音配音 ===== -->
+
+        <section class="vg-step-card" [attr.data-state]="vgGetStepState(5)" [class.is-collapsed]="!vgIsStepExpanded(5)">
+
+          <header class="vg-step-card__header" (click)="vgToggleStepExpand(5)">
+
+            <div class="vg-step-card__num">{{vgStep > 5 ? '✓' : '5'}}</div>
+
+            <div class="vg-step-card__meta">
+
+              <h3>{{vgStepLabels[4]}}</h3>
+
+              <p>{{vgStepSubtitles[4]}}</p>
+
+            </div>
+
+            <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(5)">{{ vgGetStepState(5) === 'done' ? '已完成' : vgGetStepState(5) === 'active' ? '进行中' : '待解锁' }}</div>
+
+            <button type="button" class="vg-step-card__edit" *ngIf="vgGetStepState(5) === 'done'" (click)="$event.stopPropagation(); vgEditStep(5)" title="返回此步骤编辑">编辑</button>
+
+            <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(5) !== 'done'" tabindex="-1" aria-label="展开/折叠">
+
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
+
+            </button>
+
+          </header>
+
+          <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(5)">
+
+
+
+          <!-- 音色选择 -->
+
+          <div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:20px;padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+            <div class="form-field" style="flex:1;min-width:180px">
+
+              <label>音色来源</label>
+
+              <select [(ngModel)]="vgVoiceMode">
+
+                <option value="system">系统音色</option>
+
+                <option value="clone">克隆音色</option>
+
+              </select>
+
+            </div>
+
+
+
+            <div class="form-field" style="flex:2;min-width:200px" *ngIf="vgVoiceMode === 'system'">
+
+              <label>系统音色</label>
+
+              <select [(ngModel)]="vgVoiceSpeakerId">
+
+                <option value="zh_female_shuangkuai-am_16k">双快女声</option>
+
+                <option value="zh_male_chunhou-am_16k">淳厚男声</option>
+
+                <option value="zh_female_maoxiaoni_emo-am_16k">猫小妮(情感)</option>
+
+                <option value="zh_male_rap-am_16k">说唱男声</option>
+
+                <option value="zh_female_wanwanxiaohe_emo-am_16k">弯弯小何(情感)</option>
+
+              </select>
+
+            </div>
+
+
+
+            <div class="form-field" style="flex:2;min-width:200px" *ngIf="vgVoiceMode === 'clone'">
+
+              <label>克隆音色 ID</label>
+
+              <select [(ngModel)]="vgVoiceTimbreId">
+
+                <option value="">请选择已克隆的音色</option>
+
+                <option *ngFor="let t of vsClonedTimbreList" [value]="t.objectId">
+
+                  {{t.name || t.objectId}}
+
+                </option>
+
+              </select>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 旁白列表预览 -->
+
+          <div style="margin-bottom:16px">
+
+            <h4 style="margin:0 0 10px;font-size:14px">待配音片段 ({{vgScriptSegments.length}})</h4>
+
+            <div style="display:flex;flex-direction:column;gap:8px;max-height:300px;overflow-y:auto">
+
+              <div *ngFor="let seg of vgScriptSegments; let i = index"
+
+                   style="display:flex;align-items:center;gap:10px;padding:10px;background:var(--bg-secondary);border-radius:8px;font-size:13px">
+
+                <span style="background:var(--accent-color);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;flex-shrink:0">{{seg.id}}</span>
+
+                <span style="flex:1;color:var(--text-primary)">{{seg.narration}}</span>
+
+                <span *ngIf="vgAudioUrls.length > i" class="icon" style="color:var(--success-color);flex-shrink:0">check_circle</span>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 配音进度 -->
+
+          <div *ngIf="vgSynthesizing" style="margin-bottom:16px;padding:16px;background:var(--bg-secondary);border-radius:10px;text-align:center">
+
+            <div class="spinner" style="margin:0 auto 8px"></div>
+
+            <p style="margin:0;font-size:13px">正在逐段合成语音... ({{vgAudioUrls.length}}/{{vgScriptSegments.length}})</p>
+
+          </div>
+
+
+
+          <!-- 已合成音频试听 -->
+
+          <div *ngIf="vgAudioUrls.length > 0 && !vgSynthesizing" style="margin-bottom:16px;padding:12px;background:var(--bg-secondary);border-radius:10px">
+
+            <h4 style="margin:0 0 8px;font-size:13px">已合成 {{vgAudioUrls.length}} 段音频</h4>
+
+            <div style="display:flex;flex-wrap:wrap;gap:8px">
+
+              <div *ngFor="let au of vgAudioUrls" style="display:flex;align-items:center;gap:6px">
+
+                <span style="font-size:11px;font-weight:600">{{au.segId}}</span>
+
+                <audio [src]="au.audioUrl" controls style="height:28px"></audio>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
+
+            <button class="btn" (click)="vgPrevStep()">
+
+              <span class="icon">arrow_back</span> 上一步
+
+            </button>
+
+            <div style="display:flex;gap:8px">
+
+              <button class="btn btn-primary" (click)="vgSynthesizeAll()"
+
+                      [disabled]="vgSynthesizing || (vgVoiceMode === 'clone' && !vgVoiceTimbreId)">
+
+                <span class="icon">{{vgSynthesizing ? 'hourglass_empty' : 'mic'}}</span>
+
+                {{vgSynthesizing ? '合成中...' : (vgAudioUrls.length > 0 ? '重新合成' : '开始配音')}}
+
+              </button>
+
+              <button class="btn btn-primary" (click)="vgConfirmVoice()" [disabled]="vgSynthesizing">
+
+                <span class="icon">arrow_forward</span> 下一步:视频合成
+
+              </button>
+
+            </div>
+
+          </div>
+
+          </div>
+
+        </section>
+
+
+
+        <!-- ===== Step 6: 视频合成 / 结果汇总 ===== -->
+
+        <section class="vg-step-card" [attr.data-state]="vgGetStepState(6)" [class.is-collapsed]="!vgIsStepExpanded(6)">
+
+          <header class="vg-step-card__header" (click)="vgToggleStepExpand(6)">
+
+            <div class="vg-step-card__num">{{vgStep > 6 ? '✓' : '6'}}</div>
+
+            <div class="vg-step-card__meta">
+
+              <h3>{{vgStepLabels[5]}}</h3>
+
+              <p>{{vgStepSubtitles[5]}}</p>
+
+            </div>
+
+            <div class="vg-step-card__chip" [attr.data-state]="vgGetStepState(6)">{{ vgGetStepState(6) === 'done' ? '已完成' : vgGetStepState(6) === 'active' ? '进行中' : '待解锁' }}</div>
+
+            <button type="button" class="vg-step-card__toggle" [class.is-disabled]="vgGetStepState(6) !== 'done'" tabindex="-1" aria-label="展开/折叠">
+
+              <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
+
+            </button>
+
+          </header>
+
+          <div class="vg-step-card__body" *ngIf="vgIsStepExpanded(6)">
+
+
+
+          <!-- 合成进度 -->
+
+          <div *ngIf="vgCompositing" style="margin-bottom:20px">
+
+            <div style="display:flex;justify-content:space-between;margin-bottom:6px">
+
+              <span style="font-size:13px">正在合成视频(下载素材 → 逐段合成 → 拼接)...</span>
+
+              <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{vgCompositingProgress}}%</span>
+
+            </div>
+
+            <div class="progress-bar" style="height:6px">
+
+              <div class="progress-fill" [style.width]="vgCompositingProgress + '%'"></div>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 合成完成:视频播放器 -->
+
+          <div *ngIf="vgFinalVideoUrl" style="margin-bottom:20px;padding:20px;background:var(--bg-secondary);border-radius:12px;border:2px solid var(--success-color)">
+
+            <div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
+
+              <span class="icon" style="color:var(--success-color);font-size:24px">check_circle</span>
+
+              <h4 style="margin:0;font-size:16px">视频合成完成</h4>
+
+            </div>
+
+            <video controls [src]="vgFinalVideoUrl" style="width:100%;max-height:500px;border-radius:8px;background:#000" preload="metadata"></video>
+
+            <div style="display:flex;gap:8px;margin-top:12px">
+
+              <a [href]="vgFinalVideoUrl" download class="btn btn-primary" style="text-decoration:none">
+
+                <span class="icon">download</span> 下载视频
+
+              </a>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 合成错误提示 -->
+
+          <div *ngIf="vgError && !vgCompositing" style="margin-bottom:16px;padding:12px 16px;background:rgba(244,67,54,0.1);border:1px solid rgba(244,67,54,0.3);border-radius:8px;color:var(--error-color);font-size:13px">
+
+            <span class="icon" style="vertical-align:middle;margin-right:4px">error</span> {{vgError}}
+
+          </div>
+
+
+
+          <!-- 结果汇总卡片 -->
+
+          <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;margin-bottom:20px">
+
+            <!-- 脚本统计 -->
+
+            <div style="padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+              <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
+
+                <span class="icon" style="color:var(--accent-color)">auto_stories</span>
+
+                <strong>分镜脚本</strong>
+
+              </div>
+
+              <p style="margin:0;font-size:24px;font-weight:600">{{vgScriptSegments.length}}</p>
+
+              <p style="margin:2px 0 0;font-size:12px;color:var(--text-secondary)">个场景片段</p>
+
+            </div>
+
+
+
+            <!-- 配图统计 -->
+
+            <div style="padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+              <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
+
+                <span class="icon" style="color:#4CAF50">image</span>
+
+                <strong>AI 配图</strong>
+
+              </div>
+
+              <p style="margin:0;font-size:24px;font-weight:600">
+
+                {{vgImageResults.filter(r => r.status === 'done').length}} / {{vgImageResults.length}}
+
+              </p>
+
+              <p style="margin:2px 0 0;font-size:12px;color:var(--text-secondary)">张图片已生成</p>
+
+            </div>
+
+
+
+            <!-- 配音统计 -->
+
+            <div style="padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+              <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
+
+                <span class="icon" style="color:#FF9800">record_voice_over</span>
+
+                <strong>语音配音</strong>
+
+              </div>
+
+              <p style="margin:0;font-size:24px;font-weight:600">
+
+                {{vgAudioUrls.length}} / {{vgScriptSegments.length}}
+
+              </p>
+
+              <p style="margin:2px 0 0;font-size:12px;color:var(--text-secondary)">段音频已合成</p>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 素材详情列表 -->
+
+          <div style="margin-bottom:20px">
+
+            <h4 style="margin:0 0 12px;font-size:14px">素材详情</h4>
+
+            <div style="display:flex;flex-direction:column;gap:10px">
+
+              <div *ngFor="let seg of vgScriptSegments; let i = index"
+
+                   style="display:flex;gap:12px;padding:12px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+                <div style="flex-shrink:0;width:120px;height:120px;border-radius:8px;overflow:hidden;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center">
+
+                  <img *ngIf="vgImageResults[i]?.imageUrl" [src]="vgImageResults[i]?.imageUrl"
+
+                       style="width:100%;height:100%;object-fit:cover">
+
+                  <span *ngIf="!vgImageResults[i]?.imageUrl" class="icon" style="font-size:28px;color:var(--text-secondary)">image</span>
+
+                </div>
+
+                <div style="flex:1;min-width:0">
+
+                  <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
+
+                    <span style="background:var(--accent-color);color:#fff;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600">{{seg.id}}</span>
+
+                  </div>
+
+                  <p style="margin:0 0 6px;font-size:13px;color:var(--text-primary);line-height:1.5">{{seg.narration}}</p>
+
+                  <div *ngIf="vgAudioUrls[i]?.audioUrl" style="margin-top:4px">
+
+                    <audio [src]="vgAudioUrls[i]?.audioUrl" controls style="height:28px;width:100%;max-width:300px"></audio>
+
+                  </div>
+
+                </div>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div style="display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px">
+
+            <button class="btn" (click)="vgPrevStep()">
+
+              <span class="icon">arrow_back</span> 上一步
+
+            </button>
+
+            <div style="display:flex;gap:8px">
+
+              <button class="btn" (click)="vgReset()">
+
+                <span class="icon">restart_alt</span> 重新开始
+
+              </button>
+
+              <button class="btn btn-primary" (click)="vgComposite()" [disabled]="vgCompositing">
+
+                <span class="icon">{{vgCompositing ? 'hourglass_empty' : 'movie_creation'}}</span>
+
+                {{vgCompositing ? '合成中...' : vgFinalVideoUrl ? '重新合成视频' : '合成视频'}}
+
+              </button>
+
+            </div>
+
+          </div>
+
+          </div>
+
+        </section>
+
+
+
+        <!-- 使用说明 -->
+
+        <div class="vg-help-card" style="margin-top:24px;padding:16px;background:var(--bg-secondary);border-radius:10px;border:1px solid var(--border-color)">
+
+          <h4 style="margin:0 0 8px;display:flex;align-items:center;gap:6px;font-size:13px">
+
+            <span class="icon" style="font-size:16px">help_outline</span> 使用说明
+
+          </h4>
+
+          <div style="font-size:12px;color:var(--text-secondary);line-height:1.8">
+
+            <div><strong>步骤1:</strong>上传需要拆解的视频源文件(支持常见视频格式)</div>
+
+            <div><strong>步骤2:</strong>系统自动调用 Whisper 进行语音识别,并通过 AI 分析视频内容</div>
+
+            <div><strong>步骤3:</strong>AI 根据转录文字稿自动生成分镜脚本,可手动编辑调整</div>
+
+            <div><strong>步骤4:</strong>使用即梦 AI 为每个分镜自动生成配图</div>
+
+            <div><strong>步骤5:</strong>选择系统音色或克隆音色,为旁白自动合成语音</div>
+
+            <div><strong>步骤6:</strong>将配图与配音合成为最终视频(需要 ffmpeg)</div>
+
+          </div>
+
+        </div>
+
+      </section>
+
+
+
+    </main>
+
+  </div>
+
+</div>
+
+
+
+<!-- 视频预览模态框 -->
+
+<div class="preview-modal-overlay" *ngIf="showPreviewModal" (click)="closePreview()">
+
+  <div class="preview-modal" (click)="$event.stopPropagation()">
+
+    <div class="preview-modal-header">
+
+      <h3>{{previewVideoTitle}}</h3>
+
+      <button class="preview-close-btn" (click)="closePreview()">
+
+        <span class="icon">close</span>
+
+      </button>
+
+    </div>
+
+
+
+    <div class="preview-modal-body">
+
+      <div class="preview-player">
+
+        <video *ngIf="previewVideoUrl" controls autoplay [src]="previewVideoUrl" class="preview-video-element">
+
+          您的浏览器不支持视频播放
+
+        </video>
+
+        <img *ngIf="!previewVideoUrl && previewVideoInfo?.poster" [src]="previewVideoInfo.poster" [alt]="previewVideoTitle" class="preview-video-element">
+
+        <div *ngIf="previewVideoInfo?.loading && !previewVideoUrl" style="padding:12px 0;color:var(--text-secondary);text-align:center;">
+
+          正在获取可播放视频地址...
+
+        </div>
+
+      </div>
+
+
+
+      <div class="preview-info" *ngIf="previewVideoInfo">
+
+        <div class="preview-info-grid">
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.filename">
+
+            <span class="preview-info-label">文件名</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.filename}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.size">
+
+            <span class="preview-info-label">文件大小</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.size | fileSize}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.duration">
+
+            <span class="preview-info-label">时长</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.duration | timeFormat}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.resolution">
+
+            <span class="preview-info-label">分辨率</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.resolution}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.format">
+
+            <span class="preview-info-label">格式</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.format | uppercase}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.source">
+
+            <span class="preview-info-label">来源</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.source === 'downloaded' ? '已下载' : previewVideoInfo.source === 'generated' ? 'AI生成' : previewVideoInfo.source === 'douyin' ? '抖音搜索' : '已上传'}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.quality">
+
+            <span class="preview-info-label">画质</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.quality}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.cost !== undefined">
+
+            <span class="preview-info-label">费用</span>
+
+            <span class="preview-info-value">¥{{previewVideoInfo.cost}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.created_at">
+
+            <span class="preview-info-label">创建时间</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.created_at | date:'yyyy-MM-dd HH:mm'}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.authorName">
+
+            <span class="preview-info-label">作者</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.authorName}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.playCount !== undefined">
+
+            <span class="preview-info-label">播放量</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.playCount | numberFormat}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.diggCount !== undefined">
+
+            <span class="preview-info-label">点赞数</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.diggCount | numberFormat}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.commentCount !== undefined">
+
+            <span class="preview-info-label">评论数</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.commentCount | numberFormat}}</span>
+
+          </div>
+
+          <div class="preview-info-item" *ngIf="previewVideoInfo.shareCount !== undefined">
+
+            <span class="preview-info-label">分享数</span>
+
+            <span class="preview-info-value">{{previewVideoInfo.shareCount | numberFormat}}</span>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="preview-description" *ngIf="previewVideoInfo.description">
+
+          <span class="preview-info-label">描述</span>
+
+          <p>{{previewVideoInfo.description}}</p>
+
+        </div>
+
+
+
+        <div class="preview-tags" *ngIf="previewVideoInfo.tags?.length > 0">
+
+          <span class="preview-info-label">标签</span>
+
+          <div class="preview-tag-list">
+
+            <span class="tag" *ngFor="let tag of previewVideoInfo.tags">#{{tag}}</span>
+
+          </div>
+
+        </div>
+
+      </div>
+
+    </div>
+
+
+
+    <div class="preview-modal-footer">
+
+      <button class="btn" (click)="closePreview()">
+
+        <span class="icon">close</span> 关闭
+
+      </button>
+
+      <button class="btn btn-primary" *ngIf="previewSourceVideo" (click)="downloadPreviewVideo()" [disabled]="previewSourceVideo.isDownloading">
+
+        <span class="icon">{{ previewSourceVideo.isDownloading ? 'hourglass_empty' : 'download' }}</span>
+
+        {{ previewSourceVideo.isDownloading ? '下载中 ' + previewSourceVideo.downloadProgress + '%' : '下载' }}
+
+      </button>
+
+      <a class="btn btn-primary" *ngIf="!previewSourceVideo && previewDownloadUrl" [href]="previewDownloadUrl" download>
+
+        <span class="icon">download</span> 下载
+
+      </a>
+
+    </div>
+
+  </div>
+
+</div>
+
+
+
+<!-- AI重塑模态框 — 多步骤流程 -->
+
+<div class="remix-modal-overlay" *ngIf="showRemixModal" (click)="closeRemixModal()">
+
+  <div class="remix-modal" (click)="$event.stopPropagation()">
+
+    <div class="remix-modal-header">
+
+      <h3>
+
+        <span class="icon">auto_awesome</span>
+
+        AI重塑 — {{remixTargetVideo?.title}}
+
+      </h3>
+
+      <button class="preview-close-btn" (click)="closeRemixModal()" [disabled]="isRemixSubmitting">
+
+        <span class="icon">close</span>
+
+      </button>
+
+    </div>
+
+
+
+    <!-- 步骤指示器 -->
+
+    <div class="remix-stepper">
+
+      <div class="remix-step-item" *ngFor="let item of getActiveRemixSteps()"
+
+           [class.active]="remixStep === item.step"
+
+           [class.done]="remixStep > item.step">
+
+        <div class="remix-step-circle">{{remixStep > item.step ? '✓' : item.step}}</div>
+
+        <span class="remix-step-text">{{item.label}}</span>
+
+      </div>
+
+    </div>
+
+
+
+    <div class="remix-modal-body">
+
+
+
+      <!-- ===== Step 1: 提取文字稿 ===== -->
+
+      <div *ngIf="remixStep === 1">
+
+        <div class="remix-section">
+
+          <label class="remix-label">重塑模式</label>
+
+          <div class="remix-mode-grid">
+
+            <div class="remix-mode-card"
+
+                 [class.active]="remixMode === 'standard'"
+
+                 (click)="setRemixMode('standard')">
+
+              <div class="remix-mode-title">标准 AI重塑</div>
+
+              <div class="remix-mode-desc">基于文字稿、AI美化和分镜脚本,批量生成多个重塑片段并自动拼接。</div>
+
+            </div>
+
+            <div class="remix-mode-card"
+
+                 [class.active]="remixMode === 'digital-human'"
+
+                 (click)="setRemixMode('digital-human')">
+
+              <div class="remix-mode-title">数字人生成</div>
+
+              <div class="remix-mode-desc">上传人物形象图,复用原视频音频生成单条数字人口播视频。</div>
+
+            </div>
+
+          </div>
+
+        </div>
+
+
+
+        <ng-container *ngIf="remixMode === 'standard'">
+
+        <div class="remix-section">
+
+          <label class="remix-label">视频文字稿</label>
+
+
+
+          <!-- 提示:已有分镜数据可跳过 -->
+
+          <div class="remix-whisper-loaded" *ngIf="remixStoryboard.length > 0">
+
+            <span class="icon" style="color:var(--success-color)">check_circle</span>
+
+            已自动加载 Whisper 处理结果({{remixStoryboard.length}} 个分镜片段),可直接跳到分镜脚本步骤。
+
+          </div>
+
+
+
+          <!-- 提示:已加载文字稿 -->
+
+          <div class="remix-whisper-loaded" *ngIf="remixTranscript && remixStoryboard.length === 0 && remixTargetVideo?.whisper">
+
+            <span class="icon" style="color:var(--accent-color)">description</span>
+
+            已加载该视频的 Whisper 转录文字稿,可编辑后进入下一步。
+
+          </div>
+
+
+
+          <!-- Whisper 识别进度 -->
+
+          <div class="remix-whisper-progress-box" *ngIf="isWhisperRunning">
+
+            <div class="remix-stitch-banner">
+
+              <span class="icon spinning" style="color:var(--accent-color)">sync</span>
+
+              <span>{{whisperStatusText}}</span>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- Whisper 识别完成提示 -->
+
+          <div class="remix-whisper-loaded" *ngIf="whisperStatusText.startsWith('✅') && !isWhisperRunning">
+
+            <span class="icon" style="color:var(--success-color)">check_circle</span>
+
+            {{whisperStatusText}}
+
+          </div>
+
+
+
+          <!-- 生成文字稿操作区(无文字稿且不在识别中时显示) -->
+
+          <div class="remix-no-transcript" *ngIf="!remixTranscript && !isWhisperRunning && !showManualTranscriptInput">
+
+            <div class="remix-no-transcript-icon">
+
+              <span class="icon" style="font-size:36px;color:var(--text-muted)">subtitles_off</span>
+
+            </div>
+
+            <div class="remix-no-transcript-text">该视频暂无文字稿,请选择生成方式</div>
+
+            <div class="remix-no-transcript-actions">
+
+              <button class="btn btn-primary" (click)="generateTranscript()">
+
+                <span class="icon">mic</span> 使用 Whisper 自动识别
+
+              </button>
+
+              <button class="btn" (click)="showManualTranscriptInput = true">
+
+                <span class="icon">edit_note</span> 手动输入文字稿
+
+              </button>
+
+            </div>
+
+          </div>
+
+
+
+          <!-- 文字稿输入/编辑区 -->
+
+          <textarea class="remix-textarea"
+
+                    *ngIf="remixTranscript || showManualTranscriptInput || remixTargetVideo?.whisper"
+
+                    [(ngModel)]="remixTranscript"
+
+                    rows="10"
+
+                    placeholder="请输入视频的文字稿内容..."></textarea>
+
+        </div>
+
+        </ng-container>
+
+
+
+        <ng-container *ngIf="remixMode === 'digital-human'">
+
+          <div class="remix-section">
+
+            <label class="remix-label">数字人形象图</label>
+
+            <div class="remix-digital-human-box">
+
+              <div class="remix-digital-human-tip">请上传一张清晰的单人照片,并选择音频来源:可使用已复刻音色合成语音,或直接使用原视频音频。</div>
+
+              <input type="file"
+
+                     id="remixDigitalHumanImageInput"
+
+                     accept="image/jpeg,image/png,image/jpg,image/gif,.jpg,.jpeg,.png,.gif"
+
+                     (change)="onRemixDigitalHumanImageSelected($event)"
+
+                     style="display:none">
+
+
+
+              <div class="remix-no-transcript" *ngIf="!remixDigitalHumanImagePreviewUrl">
+
+                <div class="remix-no-transcript-icon">
+
+                  <span class="icon" style="font-size:36px;color:var(--text-muted)">add_photo_alternate</span>
+
+                </div>
+
+                <div class="remix-no-transcript-text">上传人物形象图后即可继续</div>
+
+                <div class="remix-no-transcript-actions">
+
+                  <label for="remixDigitalHumanImageInput" class="btn btn-primary" style="cursor:pointer;display:inline-flex">
+
+                    <span class="icon">upload</span> 选择形象图
+
+                  </label>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="remix-digital-human-preview" *ngIf="remixDigitalHumanImagePreviewUrl">
+
+                <img [src]="remixDigitalHumanImagePreviewUrl" [alt]="remixDigitalHumanImageName || '数字人形象图'">
+
+                <div class="remix-digital-human-meta">
+
+                  <div>
+
+                    <div class="remix-digital-human-name">{{remixDigitalHumanImageName}}</div>
+
+                    <div class="remix-hint">支持 jpg、jpeg、png、gif,最大 20MB</div>
+
+                  </div>
+
+                  <button class="btn" (click)="clearRemixDigitalHumanImage()">
+
+                    <span class="icon">delete</span> 移除
+
+                  </button>
+
+                </div>
+
+              </div>
+
+
+
+              <div class="storyboard-error" *ngIf="remixDigitalHumanError">{{remixDigitalHumanError}}</div>
+
+            </div>
+
+          </div>
+
+
+
+          <div class="remix-section">
+
+            <label class="remix-label">音频来源</label>
+
+            <div class="remix-mode-grid">
+
+              <div class="remix-mode-card"
+
+                   [class.active]="remixDigitalHumanAudioSource === 'tts'"
+
+                   (click)="remixDigitalHumanAudioSource = 'tts'; remixDigitalHumanError = ''">
+
+                <div class="remix-mode-title">语音合成</div>
+
+                <div class="remix-mode-desc">输入文本 + 选择已复刻音色,合成语音作为数字人口播音频。</div>
+
+              </div>
+
+              <div class="remix-mode-card"
+
+                   [class.active]="remixDigitalHumanAudioSource === 'original'"
+
+                   (click)="remixDigitalHumanAudioSource = 'original'; remixDigitalHumanError = ''">
+
+                <div class="remix-mode-title">原视频音频</div>
+
+                <div class="remix-mode-desc">提取当前视频音频作为数字人口播音频(时长需小于 60 秒)。</div>
+
+              </div>
+
+            </div>
+
+
+
+            <div *ngIf="remixDigitalHumanAudioSource === 'tts'" style="margin-top:14px;display:flex;flex-direction:column;gap:12px">
+
+              <div class="form-field">
+
+                <label>音色 ID(timbreId)*</label>
+
+                <input type="text" [(ngModel)]="remixDigitalHumanTtsTimbreId"
+
+                       placeholder="在「语音合成」标签页复刻音色后获得的 objectId">
+
+              </div>
+
+
+
+              <div *ngIf="vsClonedTimbreList.length > 0" style="display:flex;flex-wrap:wrap;gap:6px">
+
+                <button class="btn" *ngFor="let t of vsClonedTimbreList"
+
+                        [class.btn-primary]="remixDigitalHumanTtsTimbreId === t.objectId"
+
+                        (click)="remixDigitalHumanTtsTimbreId = t.objectId"
+
+                        style="font-size:12px">
+
+                  {{t.name}}
+
+                </button>
+
+              </div>
+
+
+
+              <div class="form-field">
+
+                <label>口播文本 *</label>
+
+                <textarea [(ngModel)]="remixDigitalHumanTtsText" rows="4"
+
+                          placeholder="输入数字人要说的话,将使用上面选择的音色进行语音合成..."
+
+                          style="resize:vertical;min-height:80px"></textarea>
+
+              </div>
+
+            </div>
+
+          </div>
+
+        </ng-container>
+
+      </div>
+
+
+
+      <!-- ===== Step 2: AI美化 ===== -->
+
+      <div *ngIf="remixStep === 2">
+
+        <div class="remix-section">
+
+          <label class="remix-label">原始文字稿</label>
+
+          <div class="remix-original-text">{{remixTranscript}}</div>
+
+        </div>
+
+        <div class="remix-section">
+
+          <label class="remix-label">
+
+            <span class="icon" style="font-size:16px;vertical-align:middle">auto_fix_high</span>
+
+            AI美化后的文字稿
+
+          </label>
+
+          <div class="remix-hint" style="margin-bottom:8px">
+
+            已自动去除口语化词汇、整理段落结构。可手动修改。
+
+          </div>
+
+          <textarea class="remix-textarea"
+
+                    [(ngModel)]="remixBeautifiedTranscript"
+
+                    rows="10"
+
+                    placeholder="美化后的文字稿"></textarea>
+
+        </div>
+
+      </div>
+
+
+
+      <!-- ===== Step 3: 分镜脚本 ===== -->
+
+      <div *ngIf="remixStep === 3">
+
+        <div class="remix-section">
+
+          <label class="remix-label">选择风格</label>
+
+          <div class="remix-style-grid">
+
+            <div class="remix-style-card"
+
+                 *ngFor="let style of remixStyleOptions"
+
+                 [class.active]="remixStyle === style.id"
+
+                 (click)="remixStyle = style.id; generateStoryboard()">
+
+              <span class="remix-style-name">{{style.name}}</span>
+
+            </div>
+
+          </div>
+
+        </div>
+
+        <div class="remix-section">
+
+          <label class="remix-label">分镜脚本 ({{remixStoryboard.length}} 个片段)</label>
+
+          <div class="remix-hint" style="margin-bottom:8px">每段约40-45字,可编辑提示词(Prompt)以调整画面。</div>
+
+          <div class="storyboard-list">
+
+            <div class="storyboard-card" *ngFor="let seg of remixStoryboard; let i = index">
+
+              <div class="storyboard-card-head">
+
+                <span class="storyboard-id">{{seg.id}}</span>
+
+                <span class="storyboard-act">{{seg.act}}</span>
+
+                <span class="storyboard-dur">{{seg.duration}}</span>
+
+              </div>
+
+              <div class="storyboard-narration">{{seg.narration}}</div>
+
+              <textarea class="remix-textarea storyboard-prompt"
+
+                        [(ngModel)]="seg.prompt"
+
+                        rows="2"
+
+                        placeholder="英文视频提示词..."></textarea>
+
+            </div>
+
+          </div>
+
+        </div>
+
+      </div>
+
+
+
+      <!-- ===== Step 4: 生成参数 ===== -->
+
+      <div *ngIf="remixStep === 4">
+
+        <ng-container *ngIf="remixMode === 'standard'">
+
+        <div class="remix-section">
+
+          <label class="remix-label">生成参数配置</label>
+
+          <div class="remix-params">
+
+            <div class="remix-param-item">
+
+              <label class="remix-label">视频质量</label>
+
+              <select [(ngModel)]="remixQuality">
+
+                <option value="1080p">1080P (0.63元/秒)</option>
+
+                <option value="pro">Pro 1080P (1元/秒)</option>
+
+                <option value="720p">720P (0.28元/秒,当前不可用)</option>
+
+              </select>
+
+            </div>
+
+            <div class="remix-param-item">
+
+              <label class="remix-label">每段时长</label>
+
+              <select [(ngModel)]="remixFrames">
+
+                <option [ngValue]="121">5秒 (121帧)</option>
+
+                <option [ngValue]="241">10秒 (241帧)</option>
+
+              </select>
+
+            </div>
+
+            <div class="remix-param-item">
+
+              <label class="remix-label">画面比例</label>
+
+              <select [(ngModel)]="remixAspectRatio">
+
+                <option value="16:9">16:9 横屏</option>
+
+                <option value="9:16">9:16 竖屏</option>
+
+                <option value="1:1">1:1 正方形</option>
+
+                <option value="4:3">4:3</option>
+
+              </select>
+
+            </div>
+
+          </div>
+
+        </div>
+
+
+
+        <div class="remix-cost">
+
+          <span class="icon">attach_money</span>
+
+          预估费用:<strong>¥{{(remixQuality === 'pro' ? (remixFrames === 121 ? 5 : 10) : remixQuality === '1080p' ? (remixFrames === 121 ? 3.15 : 6.3) : (remixFrames === 121 ? 1.4 : 2.8)) * remixStoryboard.length | number:'1.2-2'}}</strong>
+
+          ({{remixStoryboard.length}} 个片段 × ¥{{remixQuality === 'pro' ? (remixFrames === 121 ? '5.00' : '10.00') : remixQuality === '1080p' ? (remixFrames === 121 ? '3.15' : '6.30') : (remixFrames === 121 ? '1.40' : '2.80')}}/段)
+
+        </div>
+
+        </ng-container>
+
+
+
+        <ng-container *ngIf="remixMode === 'digital-human'">
+
+          <div class="remix-section">
+
+            <label class="remix-label">数字人生成参数</label>
+
+            <div class="remix-params remix-params-digital-human">
+
+              <div class="remix-param-item">
+
+                <label class="remix-label">输出清晰度</label>
+
+                <select [(ngModel)]="remixDigitalHumanResolution">
+
+                  <option value="1080p">1080P</option>
+
+                  <option value="720p">720P</option>
+
+                </select>
+
+              </div>
+
+              <div class="remix-param-item remix-param-checkbox">
+
+                <label class="remix-checkbox-label">
+
+                  <input type="checkbox" [(ngModel)]="remixDigitalHumanFastMode">
+
+                  启用快速模式
+
+                </label>
+
+                <div class="remix-hint">720P 会优先按快速模式提交,适合更快出结果。</div>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div class="remix-section">
+
+            <label class="remix-label">音频来源确认</label>
+
+            <div *ngIf="remixDigitalHumanAudioSource === 'tts'" style="padding:12px;background:var(--bg-tertiary);border-radius:8px;border:1px solid var(--border-color);font-size:13px">
+
+              <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
+
+                <span class="icon" style="color:var(--accent-color)">record_voice_over</span>
+
+                <strong>语音合成模式</strong>
+
+              </div>
+
+              <div style="color:var(--text-secondary)">音色 ID:<code>{{remixDigitalHumanTtsTimbreId}}</code></div>
+
+              <div style="color:var(--text-secondary);margin-top:4px">口播文本:{{remixDigitalHumanTtsText.length > 60 ? remixDigitalHumanTtsText.substring(0, 60) + '...' : remixDigitalHumanTtsText}}</div>
+
+            </div>
+
+            <div *ngIf="remixDigitalHumanAudioSource === 'original'" style="padding:12px;background:var(--bg-tertiary);border-radius:8px;border:1px solid var(--border-color);font-size:13px">
+
+              <div style="display:flex;align-items:center;gap:6px">
+
+                <span class="icon" style="color:var(--accent-color)">music_note</span>
+
+                <strong>原视频音频模式</strong> — 提取当前视频音频作为数字人口播音频
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div class="remix-section">
+
+            <label class="remix-label">补充提示词</label>
+
+            <textarea class="remix-textarea"
+
+                      [(ngModel)]="remixDigitalHumanPrompt"
+
+                      rows="4"
+
+                      placeholder="可选:补充人物动作、镜头语言或风格要求..."></textarea>
+
+            <div class="remix-hint">当前音频来源:{{remixDigitalHumanAudioSource === 'tts' ? '语音合成' : '原视频提取音频'}}</div>
+
+            <div class="storyboard-error" *ngIf="remixDigitalHumanError">{{remixDigitalHumanError}}</div>
+
+          </div>
+
+
+
+          <div class="remix-cost">
+
+            <span class="icon">movie</span>
+
+            本次将生成 <strong>1</strong> 条数字人视频,输出清晰度为 <strong>{{remixDigitalHumanResolution}}</strong>,音频来源为 <strong>{{remixDigitalHumanAudioSource === 'tts' ? '语音合成' : '原视频提取音频'}}</strong>
+
+          </div>
+
+        </ng-container>
+
+      </div>
+
+
+
+      <!-- ===== Step 5: 生成中 ===== -->
+
+      <div *ngIf="remixStep === 5">
+
+        <div class="remix-section">
+
+          <div class="remix-progress-section">
+
+            <div class="remix-progress-bar-container">
+
+              <div class="remix-progress-bar-fill" [style.width]="(remixTargetVideo?.remixProgress || 0) + '%'"></div>
+
+            </div>
+
+            <div class="remix-progress-text">
+
+              {{remixStatusText}} ({{remixTargetVideo?.remixProgress | number:'1.0-0'}}%)
+
+            </div>
+
+          </div>
+
+
+
+          <div class="remix-digital-human-progress-card" *ngIf="remixMode === 'digital-human'">
+
+            <div class="remix-hint">
+
+              {{remixDigitalHumanAudioSource === 'tts'
+
+                ? '正在使用上传形象图与合成语音生成数字人视频,请稍候。'
+
+                : '正在使用上传形象图与原视频提取音频生成数字人视频,请稍候。'}}
+
+            </div>
+
+            <div class="remix-digital-human-inline-preview" *ngIf="remixDigitalHumanImagePreviewUrl">
+
+              <img [src]="remixDigitalHumanImagePreviewUrl" [alt]="remixDigitalHumanImageName || '数字人形象图'">
+
+              <div>
+
+                <div class="remix-digital-human-name">{{remixDigitalHumanImageName}}</div>
+
+                <div class="remix-hint">当前模式:数字人生成 · {{remixDigitalHumanAudioSource === 'tts' ? '语音合成' : '原视频提取音频'}}</div>
+
+              </div>
+
+            </div>
+
+          </div>
+
+
+
+          <div class="storyboard-list" style="margin-top:16px">
+
+            <ng-container *ngIf="remixMode === 'standard'">
+
+            <div class="storyboard-card" *ngFor="let seg of remixStoryboard"
+
+                 [class.generating]="seg.status === 'generating'"
+
+                 [class.completed]="seg.status === 'completed'"
+
+                 [class.failed]="seg.status === 'failed'">
+
+              <div class="storyboard-card-head">
+
+                <span class="storyboard-id">{{seg.id}}</span>
+
+                <span class="storyboard-status-icon">
+
+                  <span class="icon" *ngIf="seg.status === 'pending'" style="color:var(--text-muted)">hourglass_empty</span>
+
+                  <span class="icon" *ngIf="seg.status === 'generating'" style="color:var(--accent-color)">autorenew</span>
+
+                  <span class="icon" *ngIf="seg.status === 'completed'" style="color:var(--success-color)">check_circle</span>
+
+                  <span class="icon" *ngIf="seg.status === 'failed'" style="color:var(--error-color)">error</span>
+
+                </span>
+
+                <span class="storyboard-dur">{{seg.duration}}</span>
+
+              </div>
+
+              <div class="storyboard-narration">{{seg.narration}}</div>
+
+              <div class="storyboard-error" *ngIf="seg.error">{{seg.error}}</div>
+
+            </div>
+
+            </ng-container>
+
+          </div>
+
+        </div>
+
+      </div>
+
+
+
+      <!-- ===== Step 6: 结果汇总 ===== -->
+
+      <div *ngIf="remixStep === 6">
+
+        <div *ngIf="remixMode === 'digital-human' && remixFinalVideoUrl" class="remix-stitch-banner stitch-done">
+
+          <span class="icon" style="color:var(--success-color)">check_circle</span>
+
+          <span>数字人视频生成完成!</span>
+
+          <a [href]="remixFinalVideoUrl" target="_blank" class="storyboard-video-link" style="margin-left:auto">
+
+            <span class="icon">play_circle</span> 播放视频
+
+          </a>
+
+        </div>
+
+
+
+        <!-- 一键成片状态 -->
+
+        <div class="remix-stitch-banner" *ngIf="remixMode === 'standard' && (remixStitchStatus === 'stitching' || remixStitchStatus === 'polling')">
+
+          <span class="icon spinning" style="color:var(--accent-color)">sync</span>
+
+          <span>{{remixStatusText}}</span>
+
+        </div>
+
+        <div class="remix-stitch-banner stitch-done" *ngIf="remixMode === 'standard' && remixStitchStatus === 'done'">
+
+          <span class="icon" style="color:var(--success-color)">check_circle</span>
+
+          <span>视频拼接完成!</span>
+
+          <a [href]="remixStitchUrl" target="_blank" class="storyboard-video-link" style="margin-left:auto">
+
+            <span class="icon">play_circle</span> 播放完整视频
+
+          </a>
+
+        </div>
+
+        <div class="remix-stitch-banner stitch-failed" *ngIf="remixStitchStatus === 'failed'">
+
+          <span class="icon" style="color:var(--error-color)">warning</span>
+
+          <span>{{remixMode === 'digital-human' ? remixStitchError : '自动拼接失败:' + remixStitchError + '(各片段仍可单独查看)'}}</span>
+
+        </div>
+
+
+
+        <div class="remix-result-summary">
+
+          <div class="remix-result-stat success">
+
+            <span class="remix-result-num">{{remixResults.success}}</span>
+
+            <span class="remix-result-label">成功</span>
+
+          </div>
+
+          <div class="remix-result-stat failed" *ngIf="remixResults.failed > 0">
+
+            <span class="remix-result-num">{{remixResults.failed}}</span>
+
+            <span class="remix-result-label">失败</span>
+
+          </div>
+
+          <div class="remix-result-stat total">
+
+            <span class="remix-result-num">{{remixResults.total}}</span>
+
+            <span class="remix-result-label">总计</span>
+
+          </div>
+
+        </div>
+
+        <div class="storyboard-list" style="margin-top:16px">
+
+          <ng-container *ngIf="remixMode === 'standard'">
+
+          <div class="storyboard-card" *ngFor="let seg of remixStoryboard"
+
+               [class.completed]="seg.status === 'completed'"
+
+               [class.failed]="seg.status === 'failed'">
+
+            <div class="storyboard-card-head">
+
+              <span class="storyboard-id">{{seg.id}}</span>
+
+              <span class="icon" *ngIf="seg.status === 'completed'" style="color:var(--success-color)">check_circle</span>
+
+              <span class="icon" *ngIf="seg.status === 'failed'" style="color:var(--error-color)">error</span>
+
+            </div>
+
+            <div class="storyboard-narration">{{seg.narration}}</div>
+
+            <a *ngIf="seg.videoUrl" [href]="seg.videoUrl" target="_blank" class="storyboard-video-link">
+
+              <span class="icon">play_circle</span> 查看片段
+
+            </a>
+
+            <div class="storyboard-error" *ngIf="seg.error">{{seg.error}}</div>
+
+          </div>
+
+          </ng-container>
+
+        </div>
+
+      </div>
+
+
+
+    </div>
+
+
+
+    <!-- 底部操作按钮 -->
+
+    <div class="remix-modal-footer">
+
+      <!-- Step 1 -->
+
+      <ng-container *ngIf="remixStep === 1">
+
+        <button class="btn" (click)="closeRemixModal()">取消</button>
+
+        <button class="btn btn-accent" *ngIf="remixMode === 'standard' && remixStoryboard.length > 0" (click)="remixStep = 3">
+
+          <span class="icon">skip_next</span> 跳到分镜脚本
+
+        </button>
+
+        <button class="btn btn-primary"
+
+                (click)="goToRemixGenerationConfig()"
+
+                [disabled]="remixMode === 'digital-human' ? !remixDigitalHumanImageFile : !remixTranscript.trim()">
+
+          {{remixMode === 'digital-human' ? '下一步:生成参数' : '下一步:AI美化'}} <span class="icon">arrow_forward</span>
+
+        </button>
+
+      </ng-container>
+
+      <!-- Step 2 -->
+
+      <ng-container *ngIf="remixMode === 'standard' && remixStep === 2">
+
+        <button class="btn" (click)="remixStep = 1"><span class="icon">arrow_back</span> 上一步</button>
+
+        <button class="btn btn-primary" (click)="confirmBeautified()" [disabled]="!remixBeautifiedTranscript.trim()">
+
+          下一步:分镜脚本 <span class="icon">arrow_forward</span>
+
+        </button>
+
+      </ng-container>
+
+      <!-- Step 3 -->
+
+      <ng-container *ngIf="remixMode === 'standard' && remixStep === 3">
+
+        <button class="btn" (click)="remixStep = 2"><span class="icon">arrow_back</span> 上一步</button>
+
+        <button class="btn btn-primary" (click)="confirmStoryboard()" [disabled]="remixStoryboard.length === 0">
+
+          下一步:生成参数 <span class="icon">arrow_forward</span>
+
+        </button>
+
+      </ng-container>
+
+      <!-- Step 4 -->
+
+      <ng-container *ngIf="remixStep === 4">
+
+        <button class="btn" (click)="remixStep = remixMode === 'digital-human' ? 1 : 3"><span class="icon">arrow_back</span> 上一步</button>
+
+        <button class="btn btn-primary" (click)="startBatchGeneration()">
+
+          <span class="icon">auto_awesome</span> {{remixMode === 'digital-human' ? '开始生成数字人视频' : '开始生成 (' + remixStoryboard.length + ' 个片段)'}}
+
+        </button>
+
+      </ng-container>
+
+      <!-- Step 5 -->
+
+      <ng-container *ngIf="remixStep === 5">
+
+        <div class="remix-footer-status">{{remixStatusText}}</div>
+
+      </ng-container>
+
+      <!-- Step 6 -->
+
+      <ng-container *ngIf="remixStep === 6">
+
+        <button class="btn" (click)="closeRemixModal()">关闭</button>
+
+        <button class="btn btn-primary" (click)="finishRemix()">
+
+          <span class="icon">visibility</span> 查看生成结果
+
+        </button>
+
+      </ng-container>
+
+    </div>
+
+  </div>
+
+ </div>
+
+
+
+  <!-- AI重塑模式选择弹窗 -->
+  <div class="remix-modal-overlay remix-mode-chooser-overlay" *ngIf="showRemixModeChooser" (click)="closeRemixModeChooser()">
+    <div class="remix-mode-chooser" (click)="$event.stopPropagation()">
+      <div class="remix-mode-chooser__header">
+        <h3>
+          <span class="icon">auto_awesome</span>
+          选择重塑方式
+        </h3>
+        <button type="button" class="remix-mode-chooser__close" (click)="closeRemixModeChooser()" aria-label="关闭">
+          <span class="icon">close</span>
+        </button>
+      </div>
+      <p class="remix-mode-chooser__subtitle" *ngIf="remixModeChooserVideo">
+        当前视频:{{remixModeChooserVideo.title}}
+      </p>
+      <div class="remix-mode-chooser__grid">
+        <button type="button" class="remix-mode-chooser__card" (click)="selectRemixMode('standard')">
+          <div class="remix-mode-chooser__card-title">标准AI重塑</div>
+          <div class="remix-mode-chooser__card-desc">基于文字稿、AI美化和分镜脚本,批量生成多个重塑片段并自动拼接。</div>
+        </button>
+        <button type="button" class="remix-mode-chooser__card" (click)="selectRemixMode('digital-human')">
+          <div class="remix-mode-chooser__card-title">数字人生成</div>
+          <div class="remix-mode-chooser__card-desc">上传人物形象图,复用原视频内容生成单条数字人口播视频。</div>
+        </button>
+      </div>
+    </div>
+  </div>
+
+  <!-- 视频上传模态框 -->
+
+  <div class="remix-modal-overlay" *ngIf="showUploadModal" (click)="closeUploadModal()">
+
+    <div class="remix-modal" style="max-width:560px" (click)="$event.stopPropagation()">
+
+      <div class="remix-modal-header">
+
+        <h3>
+
+          <span class="icon">upload</span> 上传视频
+
+        </h3>
+
+        <button class="icon-btn" (click)="closeUploadModal()" [disabled]="isUploading">
+
+          <span class="icon">close</span>
+
+        </button>
+
+      </div>
+
+
+
+      <div class="remix-modal-body">
+
+        <!-- 文件选择区域 -->
+
+        <div style="border:2px dashed var(--border-color);border-radius:12px;padding:32px;text-align:center;margin-bottom:16px;transition:all 0.2s"
+
+             [style.border-color]="uploadFileName ? 'var(--accent-color)' : 'var(--border-color)'"
+
+             [style.background]="uploadFileName ? 'var(--accent-color-alpha, rgba(33,150,243,0.05))' : 'transparent'">
+
+          <input type="file" id="videoFileInput" accept="video/mp4,video/quicktime,video/x-msvideo,video/webm,video/x-matroska,.mp4,.mov,.avi,.webm,.mkv"
+
+                 (change)="onFileSelected($event)" style="display:none" [disabled]="isUploading">
+
+
+
+          <div *ngIf="!uploadFileName">
+
+            <span class="icon" style="font-size:48px;color:var(--text-secondary);display:block;margin-bottom:8px">cloud_upload</span>
+
+            <p style="margin:0 0 8px;color:var(--text-primary);font-size:15px;font-weight:500">点击选择视频文件</p>
+
+            <p style="margin:0;color:var(--text-secondary);font-size:13px">支持 MP4、MOV、AVI、WebM、MKV 格式,最大 500MB</p>
+
+            <label for="videoFileInput" class="btn btn-primary" style="margin-top:12px;cursor:pointer;display:inline-flex">
+
+              <span class="icon">folder_open</span> 选择文件
+
+            </label>
+
+          </div>
+
+
+
+          <div *ngIf="uploadFileName">
+
+            <span class="icon" style="font-size:36px;color:var(--accent-color);display:block;margin-bottom:8px">movie</span>
+
+            <p style="margin:0 0 4px;color:var(--text-primary);font-size:14px;font-weight:500">{{uploadFileName}}</p>
+
+            <p *ngIf="uploadFile" style="margin:0;color:var(--text-secondary);font-size:12px">
+
+              文件大小: {{(uploadFile.size / 1024 / 1024).toFixed(1)}} MB
+
+            </p>
+
+            <label for="videoFileInput" class="btn" style="margin-top:8px;cursor:pointer;display:inline-flex;font-size:12px" *ngIf="!isUploading">
+
+              <span class="icon" style="font-size:14px">swap_horiz</span> 更换文件
+
+            </label>
+
+          </div>
+
+        </div>
+
+
+
+        <!-- 视频信息表单 -->
+
+        <div style="display:flex;flex-direction:column;gap:12px">
+
+          <div class="form-field">
+
+            <label>视频标题</label>
+
+            <input type="text" [(ngModel)]="uploadTitle" placeholder="输入视频标题" [disabled]="isUploading">
+
+          </div>
+
+          <div class="form-field">
+
+            <label>描述</label>
+
+            <textarea [(ngModel)]="uploadDescription" placeholder="输入视频描述(可选)" rows="2"
+
+                      style="resize:vertical;min-height:48px" [disabled]="isUploading"></textarea>
+
+          </div>
+
+          <div class="form-field">
+
+            <label>标签</label>
+
+            <input type="text" [(ngModel)]="uploadTags" placeholder="用逗号分隔多个标签(可选)" [disabled]="isUploading">
+
+          </div>
+
+        </div>
+
+
+
+        <!-- 上传进度 -->
+
+        <div *ngIf="isUploading" style="margin-top:16px">
+
+          <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
+
+            <span style="font-size:13px;color:var(--text-primary)">正在上传...</span>
+
+            <span style="font-size:13px;font-weight:600;color:var(--accent-color)">{{uploadProgress}}%</span>
+
+          </div>
+
+          <div class="progress-bar" style="height:8px">
+
+            <div class="progress-fill" [style.width]="uploadProgress + '%'"
+
+                 [class.progress-done]="uploadProgress === 100"
+
+                 style="transition:width 0.3s"></div>
+
+          </div>
+
+          <p *ngIf="uploadProgress === 100" style="margin:8px 0 0;font-size:12px;color:var(--text-secondary)">
+
+            文件已传输完成,正在处理...
+
+          </p>
+
+        </div>
+
+
+
+        <!-- 错误信息 -->
+
+        <div *ngIf="uploadError" style="margin-top:12px;padding:10px 14px;background:var(--error-bg, rgba(244,67,54,0.08));border-radius:8px;color:var(--error-color);font-size:13px;display:flex;align-items:center;gap:6px">
+
+          <span class="icon" style="font-size:16px">error</span>
+
+          {{uploadError}}
+
+        </div>
+
+      </div>
+
+
+
+      <div class="remix-modal-footer">
+
+        <button class="btn" (click)="closeUploadModal()" [disabled]="isUploading">取消</button>
+
+        <button class="btn btn-primary" (click)="uploadVideo()" [disabled]="!uploadFile || isUploading">
+
+          <span class="icon">{{isUploading ? 'hourglass_empty' : 'upload'}}</span>
+
+          {{isUploading ? '上传中...' : '开始上传'}}
+
+        </button>
+
+      </div>
+
+    </div>
+
+  </div>
+
+
+
+  <!-- 历史详情模态框 -->
+
+  <div class="remix-modal-overlay" *ngIf="showHistoryDetailModal" (click)="closeHistoryDetail()">
+
+    <div class="modal remix-modal" style="max-width:800px;max-height:85vh;overflow-y:auto" (click)="$event.stopPropagation()">
+
+      <div class="modal-header">
+
+        <h3>
+
+          <span class="icon">history</span>
+
+          重塑详情 — {{historyDetailRecord?.original_video_title || historyDetailRecord?.keyword}}
+
+        </h3>
+
+        <button class="icon-btn" (click)="closeHistoryDetail()">
+
+          <span class="icon">close</span>
+
+        </button>
+
+      </div>
+
+
+
+      <div class="modal-body" style="padding:16px">
+
+        <div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px;padding:12px;background:var(--bg-tertiary);border-radius:8px">
+
+          <div *ngIf="historyDetailRecord?.styleName"><strong>风格:</strong>{{historyDetailRecord?.styleName}}</div>
+
+          <div *ngIf="historyDetailRecord?.duration"><strong>片段:</strong>{{historyDetailRecord?.duration}}</div>
+
+          <div *ngIf="historyDetailRecord?.quality"><strong>画质:</strong>{{historyDetailRecord?.quality}}</div>
+
+          <div><strong>费用:</strong>¥{{historyDetailRecord?.cost || 0}}</div>
+
+          <div><strong>状态:</strong>
+
+            <span class="status-badge" [class]="getStatusColor(historyDetailRecord?.status || '')">
+
+              {{getStatusText(historyDetailRecord?.status || '')}}
+
+            </span>
+
+          </div>
+
+          <div><strong>时间:</strong>{{historyDetailRecord?.created_at | date:'yyyy-MM-dd HH:mm'}}</div>
+
+        </div>
+
+
+
+        <div class="history-primary-video" *ngIf="historyDetailPrimaryVideoUrl">
+
+          <div class="history-primary-video-head">
+
+            <h4>
+
+              <span class="icon" style="font-size:18px;vertical-align:middle">play_circle</span>
+
+              生成结果
+
+            </h4>
+
+            <a [href]="historyDetailPrimaryVideoUrl" target="_blank" class="btn btn-sm">
+
+              <span class="icon" style="font-size:14px">open_in_new</span> 新窗口播放
+
+            </a>
+
+          </div>
+
+          <video controls [src]="historyDetailPrimaryVideoUrl" class="history-primary-video-player"></video>
+
+        </div>
+
+
+
+        <div *ngIf="historyDetailLoading" class="history-detail-loading">
+
+          正在加载详情视频与片段数据...
+
+        </div>
+
+
+
+        <h4 style="margin-bottom:12px">
+
+          <span class="icon" style="font-size:18px;vertical-align:middle">video_library</span>
+
+          视频片段 ({{historyDetailSegments.length}})
+
+        </h4>
+
+
+
+        <div *ngIf="historyDetailSegments.length === 0 && !historyDetailLoading" style="text-align:center;padding:24px;color:var(--text-secondary)">
+
+          <span class="icon" style="font-size:36px;display:block;margin-bottom:8px">info</span>
+
+          {{historyDetailPrimaryVideoUrl ? '当前记录暂无独立片段数据,已展示主视频结果' : '暂无片段数据(该记录可能未关联重塑数据)'}}
+
+        </div>
+
+
+
+        <div class="storyboard-list" style="gap:12px">
+
+          <div class="storyboard-card" *ngFor="let seg of historyDetailSegments"
+
+               [class.completed]="seg.status === 'completed'"
+
+               [class.failed]="seg.status === 'failed'"
+
+               style="margin-bottom:12px">
+
+            <div class="storyboard-card-head">
+
+              <span class="storyboard-id">{{seg.id}}</span>
+
+              <span class="status-badge" [class]="seg.status === 'completed' ? 'badge-success' : seg.status === 'failed' ? 'badge-danger' : 'badge-secondary'">
+
+                {{seg.status === 'completed' ? '成功' : seg.status === 'failed' ? '失败' : seg.status}}
+
+              </span>
+
+            </div>
+
+            <div *ngIf="seg.narration" style="font-size:13px;color:var(--text-secondary);margin:6px 0">
+
+              {{seg.narration}}
+
+            </div>
+
+            <div *ngIf="seg.videoUrl" style="margin-top:8px">
+
+              <video controls [src]="seg.videoUrl" style="width:100%;max-height:240px;border-radius:6px;background:#000"></video>
+
+            </div>
+
+            <div *ngIf="seg.videoUrl" style="margin-top:6px">
+
+              <a [href]="seg.videoUrl" target="_blank" class="btn btn-sm" style="font-size:12px">
+
+                <span class="icon" style="font-size:14px">open_in_new</span> 新窗口播放
+
+              </a>
+
+            </div>
+
+            <div *ngIf="!seg.videoUrl && seg.status === 'failed'" style="color:var(--error-color);font-size:13px;margin-top:6px">
+
+              <span class="icon" style="font-size:14px">error</span> 生成失败
+
+            </div>
+
+          </div>
+
+        </div>
+
+      </div>
+
+
+
+      <div class="modal-footer" style="padding:12px 16px;border-top:1px solid var(--border-color);display:flex;justify-content:flex-end">
+
+        <button class="btn" (click)="closeHistoryDetail()">关闭</button>
+
+      </div>
+
+    </div>
+
+  </div>
+
+
+
+  <!-- Toast 通知 -->
+
+  <div class="toast-container" *ngIf="toastVisible" [ngClass]="['toast-' + toastType]" (click)="toastVisible = false">
+
+    <span class="icon toast-icon">
+
+      {{toastType === 'success' ? 'check_circle' : toastType === 'error' ? 'error' : toastType === 'warn' ? 'warning' : 'info'}}
+
+    </span>
+
+    <span class="toast-text">{{toastMessage}}</span>
+
+    <span class="icon toast-close">close</span>
+
+  </div>
+

+ 10 - 0
src/app/app.routes.ts

@@ -0,0 +1,10 @@
+import { Routes } from '@angular/router';
+import { WorkflowComponent } from './components/workflow/workflow.component';
+import { DebugPanelComponent } from './components/debug-panel/debug-panel.component';
+
+export const routes: Routes = [
+  { path: '', component: WorkflowComponent },
+  { path: 'workflow', component: WorkflowComponent }
+,
+  { path: 'debug', component: DebugPanelComponent }
+];

+ 23 - 0
src/app/app.spec.ts

@@ -0,0 +1,23 @@
+import { TestBed } from '@angular/core/testing';
+import { App } from './app';
+
+describe('App', () => {
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [App],
+    }).compileComponents();
+  });
+
+  it('should create the app', () => {
+    const fixture = TestBed.createComponent(App);
+    const app = fixture.componentInstance;
+    expect(app).toBeTruthy();
+  });
+
+  it('should render title', async () => {
+    const fixture = TestBed.createComponent(App);
+    await fixture.whenStable();
+    const compiled = fixture.nativeElement as HTMLElement;
+    expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Tik-tok');
+  });
+});

+ 14509 - 0
src/app/app.ts

@@ -0,0 +1,14509 @@
+import { Component, signal, NgZone, ChangeDetectorRef } from '@angular/core';
+
+import { CommonModule } from '@angular/common';
+
+import { FormsModule } from '@angular/forms';
+
+import { RouterModule } from '@angular/router';
+
+import { TimeFormatPipe } from './pipes/time-format.pipe';
+
+import { NumberFormatPipe } from './pipes/number-format.pipe';
+
+import { FileSizePipe } from './pipes/file-size.pipe';
+
+import { DouyinService } from './services/douyin.service';
+
+import { JimengService } from './services/jimeng.service';
+
+import { LlmService } from './services/llm.service';
+
+import { HttpClient, HttpErrorResponse } from '@angular/common/http';
+
+import { timeout, finalize } from 'rxjs/operators';
+
+
+
+// 类型定义
+
+interface VideoInfo {
+
+  aweme_id: string;
+
+  desc: string;
+
+  cover_url: string;
+
+  cover_candidates?: string[];
+
+  duration: number;
+
+  create_time?: number;
+
+  publishDate?: Date | null;
+
+  author: {
+
+    nickname: string;
+
+    sec_uid: string;
+
+  };
+
+  statistics: {
+
+    play_count: number;
+
+    digg_count: number;
+
+    comment_count: number;
+
+    share_count: number;
+
+  };
+
+  text_extra?: string[];
+
+  // 下载相关
+
+  downloadProgress?: number;
+
+  isDownloading?: boolean;
+
+  downloadUrl?: string;
+
+}
+
+
+
+interface Task {
+
+  id: string;
+
+  keyword: string;
+
+  status: 'pending' | 'processing' | 'completed' | 'failed' | 'timeout';
+
+  step: 'search' | 'detail' | 'comments' | 'download' | 'transcribe' | 'generate' | 'complete'
+
+      | 'vg-upload' | 'vg-analyze' | 'vg-script' | 'vg-image' | 'vg-voice' | 'vg-composite';
+
+  progress: number;
+
+  cost: number;
+
+  created_at: Date;
+
+  updated_at: Date;
+
+  result_url?: string;
+
+  error_message?: string;
+
+  original_video_title?: string;
+
+  duration?: string;
+
+  // 任务类型:remix=AI重塑, video-generation=视频生成
+
+  type?: 'remix' | 'video-generation';
+
+  // 视频生成专用持久化数据
+
+  vgData?: {
+
+    videoId: string;
+
+    sourceFileName: string;
+
+    sourceVideoUrl: string;
+
+    step: number;
+
+    transcript: string;
+
+    analysisText: string;
+
+    script: string;
+
+    scriptSegments: { id: string; narration: string; imagePrompt: string; imagePromptCn?: string; }[];
+
+    imageResults: { id: string; prompt: string; imageUrl: string; status: 'pending' | 'generating' | 'done' | 'failed'; }[];
+
+    audioUrls: { segId: string; audioUrl: string; }[];
+
+    voiceMode: 'clone' | 'system';
+
+    voiceSpeakerId: string;
+
+    voiceTimbreId: string;
+
+    finalVideoUrl: string;
+
+  };
+
+}
+
+
+
+interface TaskStats {
+
+  pending: number;
+
+  processing: number;
+
+  completed: number;
+
+  failed: number;
+
+}
+
+
+
+interface VoiceProfile {
+
+  id: string;
+
+  name: string;
+
+  creation_mode?: string;
+
+  speaker_id: string;
+
+  timbre_id?: string;
+
+  sample_text: string;
+
+  text_prompt?: string;
+
+  source_audio_text?: string;
+
+  source_audio_name?: string;
+
+  source_audio_format?: string;
+
+  language: number;
+
+  status: number | null;
+
+  status_label: string;
+
+  demo_audio: string;
+
+  available_training_times: number | null;
+
+  image_prompt_name?: string;
+
+  x_api_resource_id?: string;
+
+  model_version?: string;
+
+  icl_speaker_id?: string;
+
+  occupied?: boolean;
+
+  synthesized_audio_url?: string;
+
+  synthesized_work_id?: string;
+
+  last_synthesis_text?: string;
+
+  last_synthesis_ssml?: string;
+
+  last_synthesis_x_api_resource_id?: string;
+
+  last_synthesis_model?: string;
+
+  last_synthesis_format?: string;
+
+  last_synthesis_sample_rate?: number;
+
+  last_synthesis_speech_rate?: number;
+
+  last_synthesis_loudness_rate?: number;
+
+  last_synthesis_emotion?: string;
+
+  last_synthesis_emotion_scale?: number;
+
+  last_synthesis_enable_subtitle?: boolean;
+
+  last_synthesis_silence_duration?: number;
+
+  last_synthesis_enable_language_detector?: boolean;
+
+  last_synthesis_disable_markdown_filter?: boolean;
+
+  last_synthesis_disable_emoji_filter?: boolean;
+
+  last_synthesis_explicit_language?: string;
+
+  latest_audio_url?: string;
+
+  draft_synthesis_text?: string;
+
+  draft_synthesis_ssml?: string;
+
+  draft_synthesis_x_api_resource_id?: string;
+
+  draft_synthesis_model?: string;
+
+  draft_synthesis_format?: string;
+
+  draft_synthesis_sample_rate?: number;
+
+  draft_synthesis_speech_rate?: number;
+
+  draft_synthesis_loudness_rate?: number;
+
+  draft_synthesis_emotion?: string;
+
+  draft_synthesis_emotion_scale?: number;
+
+  draft_synthesis_enable_subtitle?: boolean;
+
+  draft_synthesis_silence_duration?: number;
+
+  draft_synthesis_enable_language_detector?: boolean;
+
+  draft_synthesis_disable_markdown_filter?: boolean;
+
+  draft_synthesis_disable_emoji_filter?: boolean;
+
+  draft_synthesis_explicit_language?: string;
+
+  is_synthesizing?: boolean;
+
+  message?: string;
+
+  request_id?: string;
+
+  created_at: Date;
+
+  updated_at?: Date;
+
+}
+
+
+
+interface GeneratedResult {
+
+  id: string;
+
+  type: 'video' | 'image';
+
+  url: string;
+
+  title: string;
+
+  created_at: Date;
+
+  quality: string;
+
+  duration: string;
+
+  cost: number;
+
+  remixId?: string;
+
+  videoId?: string;
+
+  voiceProfileId?: string;
+
+  voiceProfileName?: string;
+
+  audioSourceLabel?: string;
+
+  segments?: { id: string; videoUrl: string; prompt: string; status: string }[];
+
+}
+
+
+
+interface HistoryRecord {
+
+  id: string;
+
+  original_video_title?: string;
+
+  keyword: string;
+
+  status: 'completed' | 'failed';
+
+  cost: number;
+
+  created_at: Date;
+
+  duration?: string;
+
+  remixId?: string;
+
+  videoId?: string;
+
+  styleName?: string;
+
+  segmentCount?: number;
+
+  successCount?: number;
+
+  resultUrl?: string;
+
+  quality?: string;
+
+  voiceProfileId?: string;
+
+  voiceProfileName?: string;
+
+  audioSourceLabel?: string;
+
+}
+
+
+
+interface ManagedVideo {
+
+  id: string;
+
+  title: string;
+
+  filename: string;
+
+  filepath: string;
+
+  size: number;
+
+  duration: number;
+
+  created_at: Date;
+
+  modified_at: Date;
+
+  category: string;
+
+  tags: string[];
+
+  description: string;
+
+  thumbnail?: string;
+
+  source: 'downloaded' | 'generated' | 'uploaded';
+
+  metadata: {
+
+    resolution: string;
+
+    format: string;
+
+    bitrate?: number;
+
+  };
+
+  // Whisper 转录文件路径
+
+  whisper?: {
+
+    transcript?: string;
+
+    srt?: string;
+
+    storyboard?: string;
+
+    beautified?: string;
+
+  };
+
+  // AI重塑相关字段
+
+  isRemixing?: boolean;
+
+  remixProgress?: number;
+
+  remixStyle?: string;
+
+  originalVideoId?: string;
+
+  remixVersions?: RemixVersion[];
+
+}
+
+
+
+interface RemixVersion {
+
+  id: string;
+
+  style: string;
+
+  created_at: Date;
+
+  file_path: string;
+
+  thumbnail: string;
+
+  description: string;
+
+  parameters: RemixParameters;
+
+}
+
+
+
+interface RemixParameters {
+
+  style: string;
+
+  intensity: number;
+
+  preserve_audio: boolean;
+
+  quality: 'standard' | 'high' | 'ultra';
+
+  effects: string[];
+
+}
+
+
+
+interface VideoCategory {
+
+  id: string;
+
+  name: string;
+
+  color: string;
+
+  count: number;
+
+}
+
+
+
+interface StoryboardSegment {
+
+  id: string;
+
+  act: string;
+
+  narration: string;
+
+  prompt: string;
+
+  duration: string;
+
+  videoUrl?: string;
+
+  workId?: string;
+
+  status: 'pending' | 'generating' | 'completed' | 'failed';
+
+  error?: string;
+
+}
+
+
+
+interface RemixFlowStep {
+
+  step: number;
+
+  label: string;
+
+}
+
+
+
+interface MonitorWork {
+
+  id: string;
+
+  desc: string;
+
+  coverUrl: string;
+
+  coverCandidates: string[];
+
+  duration: number;
+
+  createTime?: Date;
+
+  type: 'video' | 'image';
+
+  statistics: {
+
+    play_count: number;
+
+    digg_count: number;
+
+    comment_count: number;
+
+    share_count: number;
+
+  };
+
+  isDownloading?: boolean;
+
+  downloadProgress?: number;
+
+}
+
+
+
+interface MonitorAuthor {
+
+  id: string;
+
+  uid: string;
+
+  secUserId: string;
+
+  nickname: string;
+
+  uniqueId: string;
+
+  shortId: string;
+
+  signature: string;
+
+  ipLocation: string;
+
+  verifiedText: string;
+
+  avatarUrl: string;
+
+  avatarCandidates: string[];
+
+  coverUrl: string;
+
+  coverCandidates: string[];
+
+  followerCount: number;
+
+  followingCount: number;
+
+  totalFavorited: number;
+
+  awemeCount: number;
+
+  works: MonitorWork[];
+
+  worksCursor: string;
+
+  hasMoreWorks: boolean;
+
+  loadingWorks: boolean;
+
+}
+
+
+
+@Component({
+
+  selector: 'app-root',
+
+  imports: [
+
+    CommonModule,
+
+    FormsModule,
+
+    RouterModule,
+
+    TimeFormatPipe,
+
+    NumberFormatPipe,
+
+    FileSizePipe
+
+  ],
+
+  templateUrl: './app.html',
+
+  styleUrl: './app.css'
+
+})
+
+export class App {
+
+  protected readonly title = signal('抖音AI视频生成系统');
+
+
+
+  // 当前激活的标签页
+
+  currentTab: 'search' | 'monitor' | 'tasks' | 'history' | 'results' | 'videos' | 'voice-synthesis' | 'digital-human' | 'video-generation' = 'search';
+
+  private readonly TAB_STORAGE_KEY = 'tiktok.currentTab';
+
+
+
+  // 主题模式(默认浅色)
+
+  themeMode: 'day' | 'night' = 'day';
+
+  private readonly THEME_STORAGE_KEY = 'tiktok.themeMode';
+
+  private readonly TIMBRE_LIST_STORAGE_KEY = 'tiktok.vsClonedTimbreList';
+
+  private readonly MONITOR_AUTHORS_STORAGE_KEY = 'tiktok.monitorAuthors';
+
+  private readonly MONITOR_SELECTED_AUTHOR_STORAGE_KEY = 'tiktok.monitorSelectedAuthorId';
+
+
+
+  // 搜索相关属性
+
+  searchKeyword: string = '';
+
+  sortType: string = '0';
+
+  publishTime: string = '0';
+
+  filterDuration: string = '0';
+
+  isSearching: boolean = false;
+
+  searchResults: VideoInfo[] = [];
+
+  filteredSearchResults: VideoInfo[] = [];
+
+  hasMore: boolean = false;
+
+  monitorUniqueIdInput: string = '';
+
+  monitorAuthors: MonitorAuthor[] = [];
+
+  selectedMonitorAuthorId: string = '';
+
+  isAddingMonitorAuthor: boolean = false;
+
+  monitorAddError: string = '';
+
+  private readonly voiceApiToken: string = 'Bearer r:f0333969e312a40e4703e8fe4ed1c600';
+
+  private readonly voiceTtsBaseUrl: string = 'https://server.fmode.cn/api/volcengine/tts';
+
+  private readonly localVoiceTtsBaseUrl: string = '/backend/api/volcengine/tts';
+
+  voiceProfiles: VoiceProfile[] = [];
+
+  isCreatingVoiceProfile: boolean = false;
+
+  voiceDesignError: string = '';
+
+  voiceCreateMode: 'clone' | 'design' | 'synthesize' = 'clone';
+
+  voiceSourceAudioFile: File | null = null;
+
+  voiceSourceAudioName: string = '';
+
+  voiceDesignImageFile: File | null = null;
+
+  voiceDesignImageName: string = '';
+
+  preferredVoiceProfileId: string = '';
+
+  readonly fallbackVoiceSpeakerIdOptions = ['S_1g3vlU702', 'S_OLZKhZWZ1', 'S_PLZKhZWZ1', 'S_QLZKhZWZ1', 'S_ULZKhZWZ1', 'S_TLZKhZWZ1', 'S_SLZKhZWZ1', 'S_RWDjSzXZ1'];
+
+  voiceSpeakerIdOptions: string[] = [...this.fallbackVoiceSpeakerIdOptions];
+
+  voiceForm = {
+
+    displayName: '',
+
+    speakerId: 'S_1g3vlU702',
+
+    textPrompt: '',
+
+    audioText: '',
+
+    sampleText: '',
+
+    synthesisText: '',
+
+    language: 0
+
+  };
+
+  voiceSynthesisForm = {
+
+    profileId: '',
+
+    speakerId: '',
+
+    text: '',
+
+    ssml: '',
+
+    xApiResourceId: '',
+
+    model: '',
+
+    format: 'mp3',
+
+    sampleRate: 24000,
+
+    speechRate: 0,
+
+    loudnessRate: 0,
+
+    emotion: '',
+
+    emotionScale: 4,
+
+    enableSubtitle: false,
+
+    silenceDuration: 0,
+
+    enableLanguageDetector: false,
+
+    disableMarkdownFilter: false,
+
+    disableEmojiFilter: false,
+
+    explicitLanguage: ''
+
+  };
+
+  isDirectVoiceSynthesizing: boolean = false;
+
+  directVoiceSynthesisAudioUrl: string = '';
+
+  directVoiceSynthesisWorkId: string = '';
+
+  directVoiceSynthesisSpeakerId: string = '';
+
+
+
+  // 语音合成标签页状态
+
+  vsTtsMode: 'clone' | 'synthesize' = 'clone';
+
+  vsCloneForm = {
+
+    name: '',
+
+    speakerId: 'S_ULZKhZWZ1',
+
+    audioText: '',
+
+    language: 0,
+
+    demoText: '青山依旧在,几度夕阳红。'
+
+  };
+
+  vsCloneAudioFile: File | null = null;
+
+  vsCloneAudioName: string = '';
+
+  vsCloneAudioUrl: string = '';
+
+  vsCloneUploading: boolean = false;
+
+  vsCloneLoading: boolean = false;
+
+  vsCloneError: string = '';
+
+  vsCloneResult: any = null;
+
+
+
+  vsSynthForm = {
+
+    timbreId: '',
+
+    text: '白发渔樵江渚上,惯看秋月春风。一壶浊酒喜相逢,古今多少事,都付笑谈中。',
+
+    isStream: false,
+
+    format: 'mp3',
+
+    sampleRate: 24000,
+
+    speechRate: 0,
+
+    loudnessRate: 0
+
+  };
+
+  vsSynthLoading: boolean = false;
+
+  vsSynthError: string = '';
+
+  vsSynthAudioUrl: string = '';
+
+  vsSynthWorkId: string = '';
+
+  vsClonedTimbreList: { objectId: string; name: string; speakerId: string }[] = [];
+
+
+
+  // 数字人合成独立页面状态
+
+  dhStep: 'form' | 'generating' | 'done' = 'form';
+
+  dhImageFile: File | null = null;
+
+  dhImageName: string = '';
+
+  dhImagePreviewUrl: string = '';
+
+  dhImageAssetUrl: string = '';
+
+  dhAudioSource: 'tts' | 'upload' = 'tts';
+
+  dhTtsTimbreId: string = '';
+
+  dhTtsText: string = '';
+
+  dhUploadAudioFile: File | null = null;
+
+  dhUploadAudioName: string = '';
+
+  dhUploadAudioUrl: string = '';
+
+  dhUploadingAudio: boolean = false;
+
+  dhPrompt: string = '';
+
+  // 数字人补充提示词预设
+  dhPromptPresets: { label: string; value: string }[] = [
+    { label: '专业播报', value: '以新闻主播的语气朗读,吐字清晰、节奏稳定、表情自信端正。' },
+    { label: '亲切自然', value: '用朋友聊天的语气,亲切自然、面带微笑,语速适中略带轻松感。' },
+    { label: '科技感',   value: '冷静理性的科技感口吻,节奏明快、抑扬有度,强调数据与关键词。' },
+    { label: '热情带货', value: '充满活力的带货主播风格,语速略快、情绪饱满,重点处加重音强调。' },
+    { label: '温柔治愈', value: '温柔轻缓的语气,气息柔和、节奏放慢,适合情感与生活类内容。' },
+    { label: '正式商务', value: '商务正式风格,语调克制、用词严谨,仪态端庄,适合企业宣传场景。' },
+  ];
+
+  dhFastMode: boolean = false;
+
+  dhResolution: '720p' | '1080p' = '1080p';
+
+  dhError: string = '';
+
+  dhLoading: boolean = false;
+
+  dhStatusText: string = '';
+
+  dhProgress: number = 0;
+
+  dhResultVideoUrl: string = '';
+
+  dhSynthesizing: boolean = false;
+
+
+
+  // 数字人合成页:AI 对话助手(用于让用户与 AI 对话调整口播稿风格 / 优化提示词)
+
+  dhChatMessages: Array<{
+
+    role: 'user' | 'assistant';
+
+    content: string;          // 给用户看的文本(assistant 端为 explanation;user 端为输入原文)
+
+    suggestedTts?: string;    // assistant 建议的新口播文本
+
+    suggestedPrompt?: string; // assistant 建议的新补充提示词
+
+    appliedTts?: boolean;
+
+    appliedPrompt?: boolean;
+
+  }> = [];
+
+  dhChatInput: string = '';
+
+  dhChatLoading: boolean = false;
+
+  dhChatError: string = '';
+
+
+
+  // 任务管理相关属性
+
+  activeTasks: Task[] = [];
+
+  taskStats: TaskStats = {
+
+    pending: 0,
+
+    processing: 0,
+
+    completed: 0,
+
+    failed: 0
+
+  };
+
+
+
+  // 历史记录相关属性
+
+  historyRecords: HistoryRecord[] = [];
+
+  historyFilter = {
+
+    status: ''
+
+  };
+
+
+
+  // 生成结果相关属性
+
+  generatedResults: GeneratedResult[] = [];
+
+
+
+  // 视频管理相关属性
+
+  managedVideos: ManagedVideo[] = [];
+
+  videoCategories: VideoCategory[] = [];
+
+  selectedVideos: Set<string> = new Set();
+
+  videoFilter = {
+
+    category: '',
+
+    source: '',
+
+    searchTerm: ''
+
+  };
+
+  isSelectionMode: boolean = false;
+
+
+
+  // 视频上传
+
+  showUploadModal: boolean = false;
+
+  uploadTitle: string = '';
+
+  uploadDescription: string = '';
+
+  uploadTags: string = '';
+
+  isUploading: boolean = false;
+
+  uploadProgress: number = 0;
+
+  uploadError: string = '';
+
+  uploadFile: File | null = null;
+
+  uploadFileName: string = '';
+
+
+
+  // 视频预览模态框
+
+  showPreviewModal: boolean = false;
+
+  previewVideoUrl: string = '';
+
+  previewDownloadUrl: string = '';
+
+  previewVideoTitle: string = '';
+
+  previewVideoInfo: any = null;
+
+  previewSourceVideo: VideoInfo | null = null;
+
+
+
+  // 搜索游标(分页)
+
+  searchCursor: number = 0;
+
+  private searchId: string = '';
+
+  private searchBacktrace: string = '';
+
+
+
+  // 视频文件基础路径
+
+  private readonly VIDEO_BASE_PATH = '/video';
+
+
+
+  // AI重塑模式选择弹窗(视频管理页 → 选择 标准AI重塑 / 数字人生成)
+
+  showRemixModeChooser: boolean = false;
+
+  remixModeChooserVideo: ManagedVideo | null = null;
+
+
+
+  // ===== 数字人合成页:参考视频(与视频生成页 vg* 完全隔离的独立状态) =====
+
+  dhRefFile: File | null = null;
+
+  dhRefFileName: string = '';
+
+  dhRefFileSize: number = 0;
+
+  dhRefVideoUrl: string = '';
+
+  dhRefVideoId: string = '';
+
+  dhRefUploading: boolean = false;
+
+  dhRefUploadProgress: number = 0;
+
+  dhRefAnalyzing: boolean = false;
+
+  dhRefAnalysisText: string = '';
+
+  dhRefTranscript: string = '';
+
+  dhRefError: string = '';
+
+  dhRefRewriting: boolean = false;
+
+  dhRefRewriteError: string = '';
+
+  // 当通过「AI 重塑 → 数字人生成」入口进入时,标记需要在分析完成后自动重塑
+
+  dhRefAutoRewrite: boolean = false;
+
+
+  // AI重塑模态框 — 多步骤流程
+
+  showRemixModal: boolean = false;
+
+  remixTargetVideo: ManagedVideo | null = null;
+
+  remixStep: number = 1; // 1提取文字稿 2AI美化 3分镜脚本 4生成参数 5生成中 6结果
+
+  remixTranscript: string = '';
+
+  remixBeautifiedTranscript: string = '';
+
+  remixStoryboard: StoryboardSegment[] = [];
+
+  remixStyle: string = 'anime';
+
+  remixQuality: '720p' | '1080p' | 'pro' = '1080p';
+
+  remixFrames: number = 121;
+
+  remixAspectRatio: string = '16:9';
+
+  remixStatusText: string = '';
+
+  isRemixSubmitting: boolean = false;
+
+  remixResults: { total: number; success: number; failed: number } = { total: 0, success: 0, failed: 0 };
+
+  remixStitchStatus: 'idle' | 'stitching' | 'polling' | 'done' | 'failed' = 'idle';
+
+  remixStitchUrl: string = '';
+
+  remixStitchError: string = '';
+
+  remixFinalVideoUrl: string = '';
+
+  remixMode: 'standard' | 'digital-human' = 'standard';
+
+  remixDigitalHumanImageFile: File | null = null;
+
+  remixDigitalHumanImageName: string = '';
+
+  remixDigitalHumanImagePreviewUrl: string = '';
+
+  remixDigitalHumanImageAssetUrl: string = '';
+
+  remixDigitalHumanPrompt: string = '';
+
+  remixDigitalHumanFastMode: boolean = false;
+
+  remixDigitalHumanResolution: '720p' | '1080p' = '1080p';
+
+  remixDigitalHumanError: string = '';
+
+  remixDigitalHumanAudioSource: 'original' | 'tts' = 'tts';
+
+  remixDigitalHumanTtsText: string = '';
+
+  remixDigitalHumanTtsTimbreId: string = '';
+
+  remixDigitalHumanTtsSynthesizing: boolean = false;
+
+
+
+  // ===== 视频生成工作流 =====
+
+  vgStep: number = 1; // 1上传视频 2视频解析 3脚本生成 4配图生成 5配音 6数字人生成
+
+  readonly vgStepLabels = ['上传视频', '视频解析', '脚本生成', '配图生成', '语音配音', '视频合成'];
+
+  readonly vgStepSubtitles = [
+
+    '选择需要拆解分析的视频源文件',
+
+    'AI 自动转录与内容理解',
+
+    'AI 生成分镜旁白与画面提示词',
+
+    '为每个分镜生成 AI 配图',
+
+    '逐段合成旁白音频',
+
+    '汇总素材并合成最终视频'
+
+  ];
+
+  // 用户手动展开的「已完成」步骤集合(active 步骤始终展开,locked 步骤始终折叠)
+
+  vgUserExpandedSteps: Set<number> = new Set<number>();
+
+  vgSourceFile: File | null = null;
+
+  vgSourceFileName: string = '';
+
+  vgSourceVideoUrl: string = '';
+
+  vgUploading: boolean = false;
+
+  vgUploadProgress: number = 0;
+
+  vgVideoId: string = '';
+
+  // Step 2: 视频解析
+
+  vgAnalyzing: boolean = false;
+
+  vgAnalysisText: string = '';
+
+  vgTranscript: string = '';
+
+  // Step 3: 脚本生成
+
+  vgGeneratingScript: boolean = false;
+
+  vgScript: string = '';
+
+  vgScriptSegments: { id: string; narration: string; imagePrompt: string; imagePromptCn?: string; }[] = [];
+
+  // Step 4: 配图生成
+
+  vgGeneratingImages: boolean = false;
+
+  vgImageResults: { id: string; prompt: string; imageUrl: string; status: 'pending' | 'generating' | 'done' | 'failed'; }[] = [];
+
+  vgImageProgress: number = 0;
+
+  // Step 5: 语音配音
+
+  vgVoiceMode: 'clone' | 'system' = 'system';
+
+  vgVoiceTimbreId: string = '';
+
+  vgVoiceSpeakerId: string = 'zh_female_shuangkuai-am_16k';
+
+  vgSynthesizing: boolean = false;
+
+  vgAudioUrls: { segId: string; audioUrl: string; }[] = [];
+
+  // Step 6: 视频合成
+
+  vgCompositing: boolean = false;
+
+  vgCompositingProgress: number = 0;
+
+  vgFinalVideoUrl: string = '';
+
+  vgError: string = '';
+
+  vgTaskId: string = ''; // 关联任务管理中的任务ID
+
+
+
+  // Whisper 文字稿生成
+
+  isWhisperRunning: boolean = false;
+
+  whisperStatusText: string = '';
+
+  showManualTranscriptInput: boolean = false;
+
+
+
+  // 当前 AI 重塑关联的任务
+
+  currentRemixTaskId: string = '';
+
+  currentRemixId: string = '';  // 当前重塑会话ID
+
+  private taskSaveReady: Promise<void> = Promise.resolve();
+
+
+
+  // 历史详情模态框
+
+  showHistoryDetailModal: boolean = false;
+
+  historyDetailRecord: HistoryRecord | null = null;
+
+  historyDetailSegments: { id: string; videoUrl: string; prompt: string; status: string; narration?: string }[] = [];
+
+  historyDetailPrimaryVideoUrl: string = '';
+
+  historyDetailLoading: boolean = false;
+
+
+
+  // Toast 通知
+
+  toastMessage: string = '';
+
+  toastType: 'success' | 'info' | 'warn' | 'error' = 'info';
+
+  toastVisible: boolean = false;
+
+  private toastTimer: any = null;
+
+
+
+  readonly remixStepLabels = ['提取文字稿', 'AI美化', '分镜脚本', '生成参数', '视频生成', '结果汇总'];
+
+  readonly digitalHumanStepLabels = ['模式选择', '生成参数', '视频生成', '结果汇总'];
+
+
+
+  // 重塑风格选项
+
+  readonly remixStyleOptions = [
+
+    { id: 'anime', name: '动漫风格', prompt: 'anime style, Japanese animation, vibrant colors, cel shading' },
+
+    { id: 'oil-painting', name: '油画风格', prompt: 'oil painting style, thick brushstrokes, rich textures, classical art' },
+
+    { id: 'watercolor', name: '水彩风格', prompt: 'watercolor painting style, soft colors, fluid brushwork, artistic' },
+
+    { id: 'cyberpunk', name: '赛博朋克', prompt: 'cyberpunk style, neon lights, futuristic city, high tech low life, dark atmosphere' },
+
+    { id: 'cinematic', name: '电影质感', prompt: 'cinematic style, dramatic lighting, shallow depth of field, movie grade color grading' },
+
+    { id: '3d-render', name: '3D渲染', prompt: '3D rendered style, realistic materials, ray tracing, studio lighting' },
+
+    { id: 'ink-wash', name: '水墨风格', prompt: 'Chinese ink wash painting style, minimalist, elegant brushwork, traditional art' },
+
+    { id: 'vintage', name: '复古风格', prompt: 'vintage retro style, film grain, warm tones, nostalgic atmosphere, 1970s aesthetic' }
+
+  ];
+
+  constructor(private douyinService: DouyinService, private jimengService: JimengService, private llmService: LlmService, private http: HttpClient, private ngZone: NgZone, private cdr: ChangeDetectorRef) {
+
+    this.initializeDefaults();
+
+    this.restoreTheme();
+
+    this.restoreCurrentTab();
+
+    this.restoreClonedTimbreList();
+
+    this.restorePersistedMonitorState();
+
+    this.refreshIncompleteMonitorAuthors();
+
+    this.loadVideoManifest();
+
+    this.loadPersistedData();
+
+  }
+
+
+
+  // 初始化默认值(非持久化数据)
+
+  private initializeDefaults(): void {
+
+    this.searchResults = [];
+
+    this.filteredSearchResults = [];
+
+    this.monitorUniqueIdInput = '';
+
+    this.monitorAuthors = [];
+
+    this.selectedMonitorAuthorId = '';
+
+    this.monitorAddError = '';
+
+    this.voiceProfiles = [];
+
+    this.voiceDesignError = '';
+
+    this.voiceCreateMode = 'clone';
+
+    this.voiceSourceAudioFile = null;
+
+    this.voiceSourceAudioName = '';
+
+    this.voiceDesignImageFile = null;
+
+    this.voiceDesignImageName = '';
+
+    this.voiceSpeakerIdOptions = [...this.fallbackVoiceSpeakerIdOptions];
+
+    this.voiceForm = this.buildDefaultVoiceForm();
+
+    this.voiceSynthesisForm = this.buildDefaultVoiceSynthesisForm();
+
+    this.taskStats = { pending: 0, processing: 0, completed: 0, failed: 0 };
+
+
+
+    // 视频分类
+
+    this.videoCategories = [
+
+      { id: 'downloaded', name: '已下载', color: '#2196F3', count: 0 },
+
+      { id: 'generated', name: 'AI生成', color: '#4CAF50', count: 0 },
+
+      { id: 'uploaded', name: '已上传', color: '#FF9800', count: 0 }
+
+    ];
+
+
+
+    this.managedVideos = [];
+
+  }
+
+
+
+  private buildDefaultVoiceForm(): typeof this.voiceForm {
+
+    return {
+
+      displayName: '',
+
+      speakerId: this.voiceSpeakerIdOptions[0] || this.fallbackVoiceSpeakerIdOptions[0] || '',
+
+      textPrompt: '',
+
+      audioText: '',
+
+      sampleText: '',
+
+      synthesisText: '',
+
+      language: 0
+
+    };
+
+  }
+
+
+
+  private buildDefaultVoiceSynthesisForm(profileId = ''): typeof this.voiceSynthesisForm {
+
+    return {
+
+      profileId,
+
+      speakerId: '',
+
+      text: '',
+
+      ssml: '',
+
+      xApiResourceId: '',
+
+      model: '',
+
+      format: 'mp3',
+
+      sampleRate: 24000,
+
+      speechRate: 0,
+
+      loudnessRate: 0,
+
+      emotion: '',
+
+      emotionScale: 4,
+
+      enableSubtitle: false,
+
+      silenceDuration: 0,
+
+      enableLanguageDetector: false,
+
+      disableMarkdownFilter: false,
+
+      disableEmojiFilter: false,
+
+      explicitLanguage: ''
+
+    };
+
+  }
+
+
+
+  private buildVoiceSynthesisFormFromProfile(profile: VoiceProfile | null | undefined): typeof this.voiceSynthesisForm {
+
+    if (!profile) {
+
+      return this.buildDefaultVoiceSynthesisForm();
+
+    }
+
+
+
+    return {
+
+      profileId: profile.id,
+
+      speakerId: String(profile.icl_speaker_id || profile.speaker_id || ''),
+
+      text: String(profile.draft_synthesis_text || profile.last_synthesis_text || ''),
+
+      ssml: String(profile.draft_synthesis_ssml || ''),
+
+      xApiResourceId: String(profile.draft_synthesis_x_api_resource_id || profile.x_api_resource_id || ''),
+
+      model: String(profile.draft_synthesis_model || ''),
+
+      format: this.normalizeVoiceSynthesisFormat(profile.draft_synthesis_format),
+
+      sampleRate: this.normalizeVoiceSynthesisSampleRate(profile.draft_synthesis_sample_rate),
+
+      speechRate: this.clampVoiceSynthesisRate(profile.draft_synthesis_speech_rate, 0),
+
+      loudnessRate: this.clampVoiceSynthesisRate(profile.draft_synthesis_loudness_rate, 0),
+
+      emotion: String(profile.draft_synthesis_emotion || ''),
+
+      emotionScale: this.clampVoiceSynthesisEmotionScale(profile.draft_synthesis_emotion_scale, 4),
+
+      enableSubtitle: !!profile.draft_synthesis_enable_subtitle,
+
+      silenceDuration: this.clampVoiceSynthesisSilenceDuration(profile.draft_synthesis_silence_duration, 0),
+
+      enableLanguageDetector: !!profile.draft_synthesis_enable_language_detector,
+
+      disableMarkdownFilter: !!profile.draft_synthesis_disable_markdown_filter,
+
+      disableEmojiFilter: !!profile.draft_synthesis_disable_emoji_filter,
+
+      explicitLanguage: String(profile.draft_synthesis_explicit_language || '')
+
+    };
+
+  }
+
+
+
+  private syncVoiceSynthesisFormSelection(): void {
+
+    const availableProfiles = this.getSynthesisVoiceProfiles();
+
+    const selectedProfile = availableProfiles.find((profile) => profile.id === this.voiceSynthesisForm.profileId) || null;
+
+    if (selectedProfile) {
+
+      this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(selectedProfile);
+
+      return;
+
+    }
+
+
+
+    if (String(this.voiceSynthesisForm.speakerId || '').trim()) {
+
+      this.voiceSynthesisForm = {
+
+        ...this.voiceSynthesisForm,
+
+        profileId: ''
+
+      };
+
+      return;
+
+    }
+
+
+
+    this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(availableProfiles[0] || null);
+
+  }
+
+
+
+  private normalizeVoiceSynthesisFormat(value: any): string {
+
+    const format = String(value || '').trim().toLowerCase();
+
+    return ['mp3', 'ogg_opus', 'pcm'].includes(format) ? format : 'mp3';
+
+  }
+
+
+
+  private normalizeVoiceSynthesisSampleRate(value: any): number {
+
+    const sampleRate = Number(value);
+
+    return [8000, 16000, 22050, 24000, 32000, 44100, 48000].includes(sampleRate) ? sampleRate : 24000;
+
+  }
+
+
+
+  private clampVoiceSynthesisRate(value: any, fallback = 0): number {
+
+    const numericValue = Number(value);
+
+    if (!Number.isFinite(numericValue)) {
+
+      return fallback;
+
+    }
+
+    return Math.min(100, Math.max(-50, numericValue));
+
+  }
+
+
+
+  private clampVoiceSynthesisEmotionScale(value: any, fallback = 4): number {
+
+    const numericValue = Number(value);
+
+    if (!Number.isFinite(numericValue)) {
+
+      return fallback;
+
+    }
+
+    return Math.min(5, Math.max(1, Math.round(numericValue)));
+
+  }
+
+
+
+  private clampVoiceSynthesisSilenceDuration(value: any, fallback = 0): number {
+
+    const numericValue = Number(value);
+
+    if (!Number.isFinite(numericValue)) {
+
+      return fallback;
+
+    }
+
+    return Math.min(30000, Math.max(0, Math.round(numericValue)));
+
+  }
+
+
+
+  private inferVoiceSourceAudioFormat(file: File): string {
+
+    const mimeType = String(file?.type || '').toLowerCase();
+
+    const fileName = String(file?.name || '').toLowerCase();
+
+    if (mimeType.includes('wav') || fileName.endsWith('.wav')) {
+
+      return 'wav';
+
+    }
+
+    if (mimeType.includes('m4a') || mimeType.includes('mp4') || fileName.endsWith('.m4a')) {
+
+      return 'm4a';
+
+    }
+
+    if (mimeType.includes('aac') || fileName.endsWith('.aac')) {
+
+      return 'aac';
+
+    }
+
+    if (mimeType.includes('flac') || fileName.endsWith('.flac')) {
+
+      return 'flac';
+
+    }
+
+    if (mimeType.includes('opus') || fileName.endsWith('.opus')) {
+
+      return 'opus';
+
+    }
+
+    if (mimeType.includes('ogg') || fileName.endsWith('.ogg')) {
+
+      return 'ogg';
+
+    }
+
+    if (mimeType.includes('pcm') || fileName.endsWith('.pcm')) {
+
+      return 'pcm';
+
+    }
+
+    return 'mp3';
+
+  }
+
+
+
+  private readFileAsBase64(file: File): Promise<string> {
+
+    return new Promise((resolve, reject) => {
+
+      const reader = new FileReader();
+
+      reader.onload = () => {
+
+        const result = String(reader.result || '');
+
+        const base64 = result.includes(',') ? result.split(',').pop() || '' : result;
+
+        if (!base64) {
+
+          reject(new Error('音频文件读取失败'));
+
+          return;
+
+        }
+
+        resolve(base64);
+
+      };
+
+      reader.onerror = () => reject(reader.error || new Error('音频文件读取失败'));
+
+      reader.readAsDataURL(file);
+
+    });
+
+  }
+
+
+
+  private buildVoiceSynthesisDraft(profile: any): Partial<VoiceProfile> {
+
+    return {
+
+      draft_synthesis_text: String(profile?.last_synthesis_text || ''),
+
+      draft_synthesis_ssml: String(profile?.last_synthesis_ssml || ''),
+
+      draft_synthesis_x_api_resource_id: String(profile?.last_synthesis_x_api_resource_id || profile?.x_api_resource_id || ''),
+
+      draft_synthesis_model: String(profile?.last_synthesis_model || ''),
+
+      draft_synthesis_format: this.normalizeVoiceSynthesisFormat(profile?.last_synthesis_format),
+
+      draft_synthesis_sample_rate: this.normalizeVoiceSynthesisSampleRate(profile?.last_synthesis_sample_rate),
+
+      draft_synthesis_speech_rate: this.clampVoiceSynthesisRate(profile?.last_synthesis_speech_rate, 0),
+
+      draft_synthesis_loudness_rate: this.clampVoiceSynthesisRate(profile?.last_synthesis_loudness_rate, 0),
+
+      draft_synthesis_emotion: String(profile?.last_synthesis_emotion || ''),
+
+      draft_synthesis_emotion_scale: this.clampVoiceSynthesisEmotionScale(profile?.last_synthesis_emotion_scale, 4),
+
+      draft_synthesis_enable_subtitle: !!profile?.last_synthesis_enable_subtitle,
+
+      draft_synthesis_silence_duration: this.clampVoiceSynthesisSilenceDuration(profile?.last_synthesis_silence_duration, 0),
+
+      draft_synthesis_enable_language_detector: !!profile?.last_synthesis_enable_language_detector,
+
+      draft_synthesis_disable_markdown_filter: !!profile?.last_synthesis_disable_markdown_filter,
+
+      draft_synthesis_disable_emoji_filter: !!profile?.last_synthesis_disable_emoji_filter,
+
+      draft_synthesis_explicit_language: String(profile?.last_synthesis_explicit_language || '')
+
+    };
+
+  }
+
+
+
+  private restoreCurrentTab(): void {
+
+    try {
+
+      const savedTab = localStorage.getItem(this.TAB_STORAGE_KEY);
+
+      if (savedTab === 'search' || savedTab === 'monitor' || savedTab === 'tasks' || savedTab === 'history' || savedTab === 'results' || savedTab === 'videos' || savedTab === 'voice-synthesis' || savedTab === 'digital-human' || savedTab === 'video-generation') {
+
+        this.currentTab = savedTab;
+
+      }
+
+    } catch {}
+
+  }
+
+
+
+  private restoreTheme(): void {
+
+    try {
+
+      const saved = localStorage.getItem(this.THEME_STORAGE_KEY);
+
+      if (saved === 'day' || saved === 'night') {
+
+        this.themeMode = saved;
+
+      }
+
+    } catch {}
+
+  }
+
+
+
+  toggleTheme(): void {
+
+    this.themeMode = this.themeMode === 'day' ? 'night' : 'day';
+
+    try {
+
+      localStorage.setItem(this.THEME_STORAGE_KEY, this.themeMode);
+
+    } catch {}
+
+  }
+
+
+
+  private restoreClonedTimbreList(): void {
+
+    try {
+
+      const saved = localStorage.getItem(this.TIMBRE_LIST_STORAGE_KEY);
+
+      if (saved) {
+
+        const parsed = JSON.parse(saved);
+
+        if (Array.isArray(parsed)) {
+
+          this.vsClonedTimbreList = parsed.filter(
+
+            (t: any) => t && typeof t.objectId === 'string' && t.objectId
+
+          );
+
+        }
+
+      }
+
+    } catch {}
+
+  }
+
+
+
+  private saveClonedTimbreList(): void {
+
+    try {
+
+      localStorage.setItem(this.TIMBRE_LIST_STORAGE_KEY, JSON.stringify(this.vsClonedTimbreList));
+
+    } catch {}
+
+  }
+
+
+
+  setCurrentTab(tab: 'search' | 'monitor' | 'tasks' | 'history' | 'results' | 'videos' | 'voice-synthesis' | 'digital-human' | 'video-generation'): void {
+
+    this.currentTab = tab;
+
+    try {
+
+      localStorage.setItem(this.TAB_STORAGE_KEY, tab);
+
+    } catch {}
+
+  }
+
+
+
+  private restorePersistedMonitorState(): void {
+
+    try {
+
+      const savedAuthors = localStorage.getItem(this.MONITOR_AUTHORS_STORAGE_KEY);
+
+      const savedSelectedAuthorStorageKey = localStorage.getItem(this.MONITOR_SELECTED_AUTHOR_STORAGE_KEY) || '';
+
+
+
+      if (savedAuthors) {
+
+        const parsedAuthors = JSON.parse(savedAuthors);
+
+        if (Array.isArray(parsedAuthors)) {
+
+          this.monitorAuthors = parsedAuthors.map((author: any) => this.deserializeMonitorAuthor(author));
+
+        }
+
+      }
+
+
+
+      this.selectedMonitorAuthorId = this.monitorAuthors.some(author => author.id === savedSelectedAuthorStorageKey)
+
+        ? savedSelectedAuthorStorageKey
+
+        : '';
+
+    } catch {
+
+      this.monitorAuthors = [];
+
+      this.selectedMonitorAuthorId = '';
+
+    }
+
+  }
+
+
+
+  private persistMonitorState(): void {
+
+    try {
+
+      localStorage.setItem(this.MONITOR_AUTHORS_STORAGE_KEY, JSON.stringify(this.monitorAuthors.map(author => this.serializeMonitorAuthor(author))));
+
+      localStorage.setItem(this.MONITOR_SELECTED_AUTHOR_STORAGE_KEY, this.selectedMonitorAuthorId || '');
+
+    } catch {}
+
+  }
+
+
+
+  // 从后端加载持久化数据(任务、历史、结果)
+
+  private loadPersistedData(): void {
+
+    // 加载任务
+
+    this.http.get<any[]>('/backend/api/tasks').subscribe({
+
+      next: (tasks) => {
+
+        this.activeTasks = tasks.map(t => ({ ...t, created_at: new Date(t.created_at), updated_at: new Date(t.updated_at) }));
+
+        this.updateTaskStats();
+
+        console.log(`📋 已加载 ${tasks.length} 个任务`);
+
+        // 同步:将 processing 但实际已完成的任务修正
+
+        this.syncTaskStatesFromRemixes();
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        console.warn('⚠️ 无法从后端加载任务,使用空列表');
+
+        this.activeTasks = [];
+
+        this.refreshView();
+
+      }
+
+    });
+
+
+
+    // 加载历史
+
+    this.http.get<any[]>('/backend/api/history').subscribe({
+
+      next: (records) => {
+
+        this.historyRecords = records.map(r => ({ ...r, created_at: new Date(r.created_at) }));
+
+        console.log(`📜 已加载 ${records.length} 条历史记录`);
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        console.warn('⚠️ 无法从后端加载历史,使用空列表');
+
+        this.historyRecords = [];
+
+        this.refreshView();
+
+      }
+
+    });
+
+
+
+    // 加载结果
+
+    this.http.get<any[]>('/backend/api/results').subscribe({
+
+      next: (results) => {
+
+        this.generatedResults = results.map(r => ({ ...r, created_at: new Date(r.created_at) }));
+
+        console.log(`🎬 已加载 ${results.length} 个生成结果`);
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        console.warn('⚠️ 无法从后端加载结果,使用空列表');
+
+        this.generatedResults = [];
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  // 从 remix 数据同步修正任务状态(处理页面刷新后任务状态不一致)
+
+  private syncTaskStatesFromRemixes(): void {
+
+    this.http.get<Record<string, any[]>>('/backend/api/remixes').subscribe({
+
+      next: (allRemixes) => {
+
+        const remixByTaskId = new Map<string, any>();
+
+        for (const remixes of Object.values(allRemixes)) {
+
+          for (const r of remixes) {
+
+            if (r.taskId) remixByTaskId.set(r.taskId, r);
+
+          }
+
+        }
+
+
+
+        let changed = false;
+
+        for (const task of this.activeTasks) {
+
+          if (task.status === 'processing') {
+
+            const remix = remixByTaskId.get(task.id);
+
+            if (remix) {
+
+              if (remix.status === 'completed' || remix.status === 'failed') {
+
+                task.status = remix.status;
+
+                task.step = remix.status === 'completed' ? 'complete' : 'generate';
+
+                task.progress = 100;
+
+                const successCount = remix.segments?.filter((s: any) => s.status === 'completed').length || 0;
+
+                const totalCount = remix.segments?.length || 0;
+
+                task.error_message = remix.status === 'failed' ? `${successCount}/${totalCount} 片段成功` : undefined;
+
+                task.result_url = remix.segments?.find((s: any) => s.videoUrl)?.videoUrl;
+
+                task.updated_at = new Date(remix.updated_at || remix.created_at);
+
+                this.updateTask(task);
+
+                changed = true;
+
+              } else if (remix.status === 'generating') {
+
+                const successCount = remix.segments?.filter((s: any) => s.status === 'completed').length || 0;
+
+                const failedCount = remix.segments?.filter((s: any) => s.status === 'failed').length || 0;
+
+                const totalCount = remix.segments?.length || 0;
+
+                const doneCount = successCount + failedCount;
+
+                task.progress = Math.round((doneCount / totalCount) * 80);
+
+                task.updated_at = new Date(remix.updated_at || remix.created_at);
+
+                this.updateTask(task);
+
+                changed = true;
+
+              }
+
+            }
+
+          }
+
+        }
+
+
+
+        // 超过1小时仍为 processing 且无匹配 remix 的任务标记为超时
+
+        const oneHourAgo = Date.now() - 60 * 60 * 1000;
+
+        for (const task of this.activeTasks) {
+
+          if (task.status === 'processing' && !remixByTaskId.has(task.id)) {
+
+            if (new Date(task.updated_at).getTime() < oneHourAgo) {
+
+              task.status = 'timeout';
+
+              task.error_message = '任务超时(无匹配生成记录)';
+
+              task.updated_at = new Date();
+
+              this.updateTask(task);
+
+              changed = true;
+
+            }
+
+          }
+
+        }
+
+
+
+        if (changed) this.updateTaskStats();
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  // 任务筛选(点击顶部统计卡可缩小到对应状态)
+
+  tasksFilter: '' | 'pending' | 'processing' | 'completed' | 'failed' = '';
+
+
+
+  getFilteredTasks(): Task[] {
+
+    if (!this.tasksFilter) return this.activeTasks;
+
+    return this.activeTasks.filter(t => t.status === this.tasksFilter);
+
+  }
+
+
+
+  toggleTasksFilter(status: '' | 'pending' | 'processing' | 'completed' | 'failed'): void {
+
+    this.tasksFilter = this.tasksFilter === status ? '' : status;
+
+    this.refreshView();
+
+  }
+
+
+
+  // 计算任务统计
+
+  private updateTaskStats(): void {
+
+    this.taskStats = {
+
+      pending: this.activeTasks.filter(t => t.status === 'pending').length,
+
+      processing: this.activeTasks.filter(t => t.status === 'processing').length,
+
+      completed: this.activeTasks.filter(t => t.status === 'completed').length,
+
+      failed: this.activeTasks.filter(t => t.status === 'failed' || t.status === 'timeout').length
+
+    };
+
+  }
+
+
+
+  // ==================== 持久化保存方法 ====================
+
+
+
+  // 保存任务到后端(返回 Promise,确保 POST 完成后再允许 PUT)
+
+  saveTask(task: Task): void {
+
+    this.taskSaveReady = new Promise<void>((resolve) => {
+
+      this.http.post<any>('/backend/api/tasks', task).subscribe({
+
+        next: (res) => {
+
+          if (res?.task?.id) task.id = res.task.id;
+
+          this.updateTaskStats();
+
+          console.log('💾 任务已保存:', task.id);
+
+          resolve();
+
+        },
+
+        error: (err) => {
+
+          console.warn('⚠️ 任务保存失败:', err);
+
+          resolve(); // 即使失败也 resolve,避免阻塞后续更新
+
+        }
+
+      });
+
+    });
+
+  }
+
+
+
+  // 更新任务到后端(等待 POST 完成后再 PUT)
+
+  updateTask(task: Task): void {
+
+    this.taskSaveReady.then(() => {
+
+      this.http.put<any>(`/backend/api/tasks/${task.id}`, task).subscribe({
+
+        next: () => {
+
+          this.updateTaskStats();
+
+        },
+
+        error: (err) => console.warn('⚠️ 任务更新失败:', err)
+
+      });
+
+    });
+
+  }
+
+
+
+  // 删除任务(先做本地乐观更新,再持久化;持久化失败也保留本地删除并提示)
+
+  removeTask(taskId: string): void {
+
+    if (!confirm('确定要删除该任务吗?此操作不可恢复。')) return;
+
+
+
+    if (!taskId) {
+
+      // ID 为空的脏数据,直接从本地移除
+
+      this.activeTasks = this.activeTasks.filter(t => !!t.id);
+
+      this.updateTaskStats();
+
+      this.refreshView();
+
+      this.showToast('🗑️ 已清除无效任务', 'info');
+
+      return;
+
+    }
+
+
+
+    // 乐观删除:立即从本地列表移除,UI 立刻更新
+
+    this.activeTasks = this.activeTasks.filter(t => t.id !== taskId);
+
+    this.canceledTaskIds.add(taskId);
+
+    this.updateTaskStats();
+
+    this.refreshView();
+
+
+
+    this.http.delete<any>(`/backend/api/tasks/${taskId}`).subscribe({
+
+      next: () => {
+
+        console.log('🗑️ 任务已删除:', taskId);
+
+        this.showToast('🗑️ 任务已删除', 'success');
+
+      },
+
+      error: (err) => {
+
+        console.warn('⚠️ 任务删除失败:', err);
+
+        this.showToast('⚠️ 后端删除失败,已从列表中移除', 'warn');
+
+      }
+
+    });
+
+  }
+
+
+
+  // 保存历史记录到后端
+
+  saveHistoryRecord(record: Partial<HistoryRecord>): void {
+
+    this.http.post<any>('/backend/api/history', record).subscribe({
+
+      next: (res) => {
+
+        if (res?.record) {
+
+          this.historyRecords.unshift({ ...res.record, created_at: new Date(res.record.created_at) });
+
+        }
+
+        console.log('💾 历史记录已保存');
+
+      },
+
+      error: (err) => console.warn('⚠️ 历史保存失败:', err)
+
+    });
+
+  }
+
+
+
+  // 删除历史记录
+
+  removeHistoryRecord(recordId: string): void {
+
+    this.http.delete<any>(`/backend/api/history/${recordId}`).subscribe({
+
+      next: () => {
+
+        this.historyRecords = this.historyRecords.filter(r => r.id !== recordId);
+
+        console.log('🗑️ 历史记录已删除:', recordId);
+
+      },
+
+      error: (err) => console.warn('⚠️ 历史删除失败:', err)
+
+    });
+
+  }
+
+
+
+  getFilteredHistoryRecords(): HistoryRecord[] {
+
+    return this.historyRecords.filter(record => {
+
+      return !this.historyFilter.status || record.status === this.historyFilter.status;
+
+    });
+
+  }
+
+
+
+  // 把标题中嵌入的 #标签 解析出来,主标题与标签分开渲染
+
+  parseHistoryRecordTitle(record: HistoryRecord): { title: string; tags: string[] } {
+
+    const raw = (record.original_video_title || record.keyword || '').trim();
+
+    if (!raw) return { title: '未命名记录', tags: [] };
+
+    const tags: string[] = [];
+
+    const cleaned = raw.replace(/#([^\s#]+)/g, (_, tag) => { tags.push(tag); return ''; }).replace(/\s+/g, ' ').trim();
+
+    return { title: cleaned || raw, tags };
+
+  }
+
+
+
+  getHistoryStatusCount(status: string): number {
+
+    if (!status) return this.historyRecords.length;
+
+    return this.historyRecords.filter(r => r.status === status).length;
+
+  }
+
+
+
+  // 保存生成结果到后端
+
+  saveResult(result: Partial<GeneratedResult>): void {
+
+    this.http.post<any>('/backend/api/results', result).subscribe({
+
+      next: (res) => {
+
+        if (res?.result) {
+
+          this.generatedResults.unshift({ ...res.result, created_at: new Date(res.result.created_at) });
+
+        }
+
+        console.log('💾 结果已保存');
+
+      },
+
+      error: (err) => console.warn('⚠️ 结果保存失败:', err)
+
+    });
+
+  }
+
+
+
+  // 删除生成结果
+
+  removeResult(resultId: string): void {
+
+    this.http.delete<any>(`/backend/api/results/${resultId}`).subscribe({
+
+      next: () => {
+
+        this.generatedResults = this.generatedResults.filter(r => r.id !== resultId);
+
+        console.log('🗑️ 结果已删除:', resultId);
+
+      },
+
+      error: (err) => console.warn('⚠️ 结果删除失败:', err)
+
+    });
+
+  }
+
+
+
+  // Toast 通知
+
+  showToast(message: string, type: 'success' | 'info' | 'warn' | 'error' = 'info', duration: number = 4000): void {
+
+    if (this.toastTimer) clearTimeout(this.toastTimer);
+
+    this.toastMessage = message;
+
+    this.toastType = type;
+
+    this.toastVisible = true;
+
+    this.toastTimer = setTimeout(() => {
+
+      this.toastVisible = false;
+
+    }, duration);
+
+  }
+
+
+
+  private refreshView(): void {
+
+    this.cdr.detectChanges();
+
+  }
+
+
+
+  // 查找并更新任务
+
+  private findRemixTask(): Task | undefined {
+
+    return this.activeTasks.find(t => t.id === this.currentRemixTaskId);
+
+  }
+
+
+
+  private updateRemixTaskProgress(step: Task['step'], progress: number, statusText?: string): void {
+
+    const task = this.findRemixTask();
+
+    if (!task) return;
+
+    task.step = step;
+
+    task.progress = Math.round(progress);
+
+    task.updated_at = new Date();
+
+    if (statusText) task.error_message = undefined;
+
+    this.updateTask(task);
+
+  }
+
+
+
+  private completeRemixTask(status: 'completed' | 'failed', resultUrl?: string, errorMsg?: string): void {
+
+    const task = this.findRemixTask();
+
+    if (!task) return;
+
+    task.status = status;
+
+    task.step = status === 'completed' ? 'complete' : task.step;
+
+    task.progress = 100;
+
+    task.result_url = resultUrl;
+
+    task.error_message = errorMsg;
+
+    task.updated_at = new Date();
+
+    this.updateTask(task);
+
+    this.updateTaskStats();
+
+  }
+
+
+
+  // ==================== Remix 会话持久化 ====================
+
+
+
+  // 保存当前重塑会话到后端(每次片段完成时调用)
+
+  private saveRemixSession(status: 'generating' | 'stitching' | 'completed' | 'failed'): void {
+
+    const video = this.remixTargetVideo;
+
+    if (!video || !this.currentRemixId) return;
+
+
+
+    const styleName = this.remixMode === 'digital-human'
+
+      ? '数字人生成'
+
+      : (this.remixStyleOptions.find(s => s.id === this.remixStyle)?.name || this.remixStyle);
+
+
+
+    const sessionData = {
+
+      remixId: this.currentRemixId,
+
+      videoId: video.id,
+
+      videoTitle: video.title,
+
+      mode: this.remixMode,
+
+      style: this.remixStyle,
+
+      styleName: styleName,
+
+      quality: this.remixMode === 'digital-human' ? this.remixDigitalHumanResolution : this.remixQuality,
+
+      frames: this.remixFrames,
+
+      aspectRatio: this.remixAspectRatio,
+
+      status: status,
+
+      taskId: this.currentRemixTaskId,
+
+      stitchUrl: this.remixStitchUrl || '',
+
+      digitalHumanAudioSource: this.remixMode === 'digital-human' ? this.remixDigitalHumanAudioSource : '',
+
+      digitalHumanVoiceProfileId: '',
+
+      digitalHumanVoiceProfileName: '',
+
+      results: { ...this.remixResults },
+
+      segments: this.remixStoryboard.map(s => ({
+
+        id: s.id,
+
+        act: s.act,
+
+        narration: s.narration,
+
+        prompt: s.prompt,
+
+        duration: s.duration,
+
+        status: s.status,
+
+        videoUrl: s.videoUrl || '',
+
+        workId: s.workId || '',
+
+        error: s.error || ''
+
+      }))
+
+    };
+
+
+
+    this.http.post<any>(`/backend/api/remixes/${video.id}`, sessionData).subscribe({
+
+      next: () => console.log(`💾 Remix 会话已保存: ${this.currentRemixId} [${status}]`),
+
+      error: (err) => console.warn('⚠️ Remix 保存失败:', err)
+
+    });
+
+  }
+
+
+
+  // 加载某视频的所有重塑记录
+
+  loadVideoRemixes(videoId: string): void {
+
+    this.http.get<any[]>(`/backend/api/remixes/${videoId}`).subscribe({
+
+      next: (remixes) => {
+
+        const video = this.managedVideos.find(v => v.id === videoId);
+
+        if (video && remixes.length > 0) {
+
+          video.remixVersions = remixes.map(r => ({
+
+            id: r.remixId,
+
+            style: r.styleName || r.style,
+
+            created_at: new Date(r.created_at),
+
+            file_path: r.stitchUrl || r.segments?.find((s: any) => s.videoUrl)?.videoUrl || '',
+
+            thumbnail: 'assets/default-remix-thumb.jpg',
+
+            description: `${r.styleName} · ${r.segments?.length || 0}段 · ${r.status === 'completed' ? '已完成' : r.status === 'generating' ? '生成中' : '失败'}`,
+
+            parameters: {
+
+              style: r.style,
+
+              intensity: 0.8,
+
+              preserve_audio: true,
+
+              quality: r.quality === 'pro' ? 'ultra' : 'high',
+
+              effects: [r.style]
+
+            }
+
+          }));
+
+          console.log(`📂 已加载 ${video.title} 的 ${remixes.length} 条重塑记录`);
+
+        }
+
+      },
+
+      error: () => {}
+
+    });
+
+  }
+
+
+
+  // 批量加载所有视频的重塑记录
+
+  private loadAllRemixes(): void {
+
+    this.http.get<Record<string, any[]>>('/backend/api/remixes').subscribe({
+
+      next: (allRemixes) => {
+
+        let total = 0;
+
+        for (const [videoId, remixes] of Object.entries(allRemixes)) {
+
+          const video = this.managedVideos.find(v => v.id === videoId);
+
+          if (video && remixes.length > 0) {
+
+            video.remixVersions = remixes.map(r => ({
+
+              id: r.remixId,
+
+              style: r.styleName || r.style,
+
+              created_at: new Date(r.created_at),
+
+              file_path: r.stitchUrl || r.segments?.find((s: any) => s.videoUrl)?.videoUrl || '',
+
+              thumbnail: 'assets/default-remix-thumb.jpg',
+
+              description: `${r.styleName} · ${r.segments?.length || 0}段 · ${r.status === 'completed' ? '已完成' : r.status === 'generating' ? '生成中' : '失败'}`,
+
+              parameters: {
+
+                style: r.style,
+
+                intensity: 0.8,
+
+                preserve_audio: true,
+
+                quality: r.quality === 'pro' ? 'ultra' : 'high',
+
+                effects: [r.style]
+
+              }
+
+            }));
+
+            total += remixes.length;
+
+          }
+
+        }
+
+        if (total > 0) console.log(`📂 已加载 ${total} 条重塑记录`);
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  // 从 src/video/manifest.json 加载视频清单
+
+  private loadVideoManifest(): void {
+
+    this.http.get<any[]>(`/backend/api/manifest`).subscribe({
+
+      next: (manifest) => {
+
+        this.managedVideos = manifest.map((item: any) => ({
+
+          id: item.id,
+
+          title: item.title || item.filename,
+
+          filename: item.filename,
+
+          filepath: (item.filepath && /^https?:\/\//.test(item.filepath)) ? item.filepath : `/backend/api/video/${item.filename}`,
+
+          size: item.size || 0,
+
+          duration: item.duration || 0,
+
+          created_at: item.created_at ? new Date(item.created_at) : new Date(),
+
+          modified_at: item.modified_at ? new Date(item.modified_at) : new Date(),
+
+          category: item.category || 'downloaded',
+
+          tags: item.tags || [],
+
+          description: item.description || '',
+
+          thumbnail: item.thumbnail || '',
+
+          source: item.source || 'downloaded',
+
+          metadata: item.metadata || { resolution: '', format: 'mp4' },
+
+          whisper: item.whisper || undefined
+
+        }));
+
+        this.updateCategoryCounts();
+
+        console.log(`📁 已加载 ${this.managedVideos.length} 个视频`);
+
+
+
+        // 加载每个视频的重塑记录
+
+        this.loadAllRemixes();
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        console.warn('⚠️ 无法加载视频清单,使用空列表:', err);
+
+        this.managedVideos = [];
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  // 将下载的视频添加到管理列表
+
+  private addToManagedVideos(video: VideoInfo, filename: string): void {
+
+    const newVideo: ManagedVideo = {
+
+      id: `VID-${Date.now()}`,
+
+      title: video.desc || filename,
+
+      filename: filename,
+
+      filepath: `${this.VIDEO_BASE_PATH}/${filename}`,
+
+      size: 0,
+
+      duration: video.duration || 0,
+
+      created_at: new Date(),
+
+      modified_at: new Date(),
+
+      category: 'downloaded',
+
+      tags: video.text_extra || [],
+
+      description: video.desc || '',
+
+      thumbnail: video.cover_url || '',
+
+      source: 'downloaded',
+
+      metadata: {
+
+        resolution: '1920x1080',
+
+        format: 'mp4'
+
+      }
+
+    };
+
+    this.managedVideos.unshift(newVideo);
+
+    this.updateCategoryCounts();
+
+    console.log(`✅ 视频已添加到管理列表: ${filename}`);
+
+  }
+
+
+
+  addMonitorAuthor(): void {
+
+    const uniqueId = this.monitorUniqueIdInput.trim();
+
+    if (!uniqueId || this.isAddingMonitorAuthor) return;
+
+
+
+    const existingAuthor = this.monitorAuthors.find(author => author.uid === uniqueId || author.id === uniqueId || author.uniqueId === uniqueId || author.shortId === uniqueId);
+
+    if (existingAuthor) {
+
+      this.selectedMonitorAuthorId = existingAuthor.id;
+
+      this.persistMonitorState();
+
+      if (existingAuthor.followerCount === 0) {
+
+        this.refreshMonitorAuthorProfile(existingAuthor, false);
+
+      }
+
+      this.showToast(`⚠️ 博主「${existingAuthor.nickname}」已在监测列表中`, 'warn');
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.isAddingMonitorAuthor = true;
+
+    this.monitorAddError = '';
+
+
+
+    this.buildCompleteMonitorAuthor(uniqueId, (author, usedFallback) => {
+
+      this.monitorAuthors = [author, ...this.monitorAuthors.filter(item => item.id !== author.id && item.uid !== author.uid)];
+
+      this.monitorUniqueIdInput = '';
+
+      this.isAddingMonitorAuthor = false;
+
+      this.persistMonitorState();
+
+      this.showToast(
+
+        usedFallback
+
+          ? `⚠️ 已添加博主「${author.nickname}」,但完整粉丝数据暂未取到`
+
+          : `✅ 已添加博主「${author.nickname}」`,
+
+        usedFallback ? 'warn' : 'success'
+
+      );
+
+      this.fetchMonitorAuthorWorks(author, false);
+
+      this.refreshView();
+
+    }, (message) => {
+
+      this.isAddingMonitorAuthor = false;
+
+      this.monitorAddError = message;
+
+      this.showToast(`❌ ${this.monitorAddError}`, 'error');
+
+      this.refreshView();
+
+    });
+
+  }
+
+
+
+  private buildCompleteMonitorAuthor(
+
+    uniqueId: string,
+
+    onSuccess: (author: MonitorAuthor, usedFallback: boolean) => void,
+
+    onError: (message: string) => void
+
+  ): void {
+
+    this.douyinService.getUserProfileByUniqueId(uniqueId).subscribe({
+
+      next: (res: any) => {
+
+        const basicProfile = this.douyinService.extractUserProfileData(res) || {};
+
+        const basicAuthor = this.mapMonitorAuthor(basicProfile, uniqueId);
+
+
+
+        if (!basicAuthor.secUserId) {
+
+          onError('未能从抖音号解析出 sec_user_id,暂时无法加载主页作品');
+
+          return;
+
+        }
+
+
+
+        this.douyinService.getUserProfileBySecUserId(basicAuthor.secUserId).subscribe({
+
+          next: (detailRes: any) => {
+
+            const detailProfile = this.douyinService.extractUserProfileData(detailRes) || {};
+
+            const author = this.mapMonitorAuthor({
+
+              ...basicProfile,
+
+              ...detailProfile,
+
+              sec_uid: detailProfile?.sec_uid || detailProfile?.sec_user_id || basicProfile?.sec_uid || basicProfile?.sec_user_id || basicAuthor.secUserId,
+
+              sec_user_id: detailProfile?.sec_user_id || detailProfile?.sec_uid || basicProfile?.sec_user_id || basicProfile?.sec_uid || basicAuthor.secUserId,
+
+              unique_id: detailProfile?.unique_id || basicProfile?.unique_id || uniqueId,
+
+              short_id: detailProfile?.short_id || basicProfile?.short_id || '',
+
+              douyin_id: detailProfile?.douyin_id || basicProfile?.douyin_id || ''
+
+            }, uniqueId);
+
+            onSuccess(author, false);
+
+          },
+
+          error: () => {
+
+            onSuccess(basicAuthor, true);
+
+          }
+
+        });
+
+      },
+
+      error: (error: any) => {
+
+        onError(this.getErrorText(error, '获取博主信息失败,请稍后重试'));
+
+      }
+
+    });
+
+  }
+
+
+
+  private refreshIncompleteMonitorAuthors(): void {
+
+    this.monitorAuthors
+
+      .filter(author => author.followerCount === 0)
+
+      .forEach(author => this.refreshMonitorAuthorProfile(author, true));
+
+  }
+
+
+
+  private refreshMonitorAuthorProfile(author: MonitorAuthor, silent: boolean): void {
+
+    const lookupId = (author.uniqueId || author.shortId || author.uid || '').trim();
+
+    if (!lookupId) {
+
+      return;
+
+    }
+
+
+
+    this.buildCompleteMonitorAuthor(lookupId, (nextAuthor, usedFallback) => {
+
+      const mergedAuthor: MonitorAuthor = {
+
+        ...author,
+
+        ...nextAuthor,
+
+        works: author.works,
+
+        worksCursor: author.worksCursor,
+
+        hasMoreWorks: author.hasMoreWorks,
+
+        loadingWorks: author.loadingWorks,
+
+        coverUrl: author.coverUrl || nextAuthor.coverUrl
+
+      };
+
+
+
+      this.monitorAuthors = this.monitorAuthors.map(item => (
+
+        item.id === author.id
+
+        || item.uid === author.uid
+
+        || (!!author.secUserId && item.secUserId === author.secUserId)
+
+        || (!!lookupId && (item.uniqueId === lookupId || item.shortId === lookupId || item.uid === lookupId))
+
+          ? mergedAuthor
+
+          : item
+
+      ));
+
+
+
+      if (this.selectedMonitorAuthorId === author.id) {
+
+        this.selectedMonitorAuthorId = mergedAuthor.id;
+
+      }
+
+
+
+      this.persistMonitorState();
+
+      if (!silent) {
+
+        this.showToast(
+
+          usedFallback
+
+            ? `⚠️ 博主「${mergedAuthor.nickname}」资料已刷新,但完整粉丝数据暂未取到`
+
+            : `✅ 博主「${mergedAuthor.nickname}」资料已刷新`,
+
+          usedFallback ? 'warn' : 'success'
+
+        );
+
+      }
+
+      this.refreshView();
+
+    }, (message) => {
+
+      if (!silent) {
+
+        this.showToast(`❌ ${message}`, 'error');
+
+      }
+
+    });
+
+  }
+
+
+
+  openMonitorAuthor(author: MonitorAuthor): void {
+
+    this.selectedMonitorAuthorId = author.id;
+
+    this.persistMonitorState();
+
+    if (author.followerCount === 0) {
+
+      this.refreshMonitorAuthorProfile(author, true);
+
+    }
+
+    if (!author.loadingWorks && author.works.length === 0) {
+
+      this.fetchMonitorAuthorWorks(author, false);
+
+    }
+
+    this.refreshView();
+
+  }
+
+
+
+  backToMonitorList(): void {
+
+    this.selectedMonitorAuthorId = '';
+
+    this.persistMonitorState();
+
+    this.refreshView();
+
+  }
+
+
+
+  removeMonitorAuthor(author: MonitorAuthor): void {
+
+    if (!confirm(`确定要删除博主「${author.nickname}」吗?`)) {
+
+      return;
+
+    }
+
+
+
+    this.monitorAuthors = this.monitorAuthors.filter(item => item.id !== author.id);
+
+
+
+    if (this.selectedMonitorAuthorId === author.id) {
+
+      this.selectedMonitorAuthorId = '';
+
+    }
+
+
+
+    this.persistMonitorState();
+
+    this.showToast(`🗑️ 已移除博主「${author.nickname}」`, 'success');
+
+    this.refreshView();
+
+  }
+
+
+
+  getSelectedMonitorAuthor(): MonitorAuthor | null {
+
+    return this.monitorAuthors.find(author => author.id === this.selectedMonitorAuthorId) || null;
+
+  }
+
+
+
+  getTotalMonitorWorksCount(): number {
+
+    return this.monitorAuthors.reduce((total, author) => total + author.works.length, 0);
+
+  }
+
+
+
+  reloadMonitorAuthorWorks(author: MonitorAuthor): void {
+
+    this.fetchMonitorAuthorWorks(author, false);
+
+  }
+
+
+
+  loadMoreMonitorWorks(author: MonitorAuthor): void {
+
+    if (!author.hasMoreWorks || author.loadingWorks) {
+
+      return;
+
+    }
+
+
+
+    this.fetchMonitorAuthorWorks(author, true);
+
+  }
+
+
+
+  handleMonitorAuthorAvatarError(author: MonitorAuthor): void {
+
+    const currentIndex = author.avatarCandidates.indexOf(author.avatarUrl);
+
+    const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
+
+    author.avatarUrl = author.avatarCandidates[nextIndex] || this.getMonitorAvatarFallback(author.nickname);
+
+    this.refreshView();
+
+  }
+
+
+
+  handleMonitorWorkCoverError(work: MonitorWork): void {
+
+    const currentIndex = work.coverCandidates.indexOf(work.coverUrl);
+
+    const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
+
+    work.coverUrl = work.coverCandidates[nextIndex] || this.getVideoCoverFallback();
+
+    this.refreshView();
+
+  }
+
+
+
+  previewMonitorWork(author: MonitorAuthor, work: MonitorWork): void {
+
+    if (work.type !== 'video') {
+
+      window.open(work.coverUrl, '_blank');
+
+      return;
+
+    }
+
+
+
+    this.viewVideoDetail(this.buildMonitorVideoInfo(author, work));
+
+  }
+
+
+
+  downloadMonitorWork(author: MonitorAuthor, work: MonitorWork): void {
+
+    if (work.type !== 'video' || work.isDownloading) return;
+
+
+
+    work.isDownloading = true;
+
+    work.downloadProgress = 0;
+
+
+
+    const video = this.buildMonitorVideoInfo(author, work);
+
+    this.douyinService.getVideoDetail(work.id).subscribe({
+
+      next: (res: any) => {
+
+        const detailData = this.douyinService.extractVideoDetailData(res) || {};
+
+        const downloadUrls = this.douyinService.extractVideoDownloadUrls(res);
+
+        const downloadUrl = downloadUrls[0] || '';
+
+
+
+        if (!downloadUrl) {
+
+          work.isDownloading = false;
+
+          work.downloadProgress = 0;
+
+          this.showToast('❌ 无法获取下载地址,请稍后重试', 'error');
+
+          this.refreshView();
+
+          return;
+
+        }
+
+
+
+        const filename = this.buildVideoFilename(video);
+
+        this.douyinService.startManagedDownload({
+
+          url: downloadUrl,
+
+          urls: downloadUrls,
+
+          filename,
+
+          title: work.desc || work.id,
+
+          description: detailData?.desc || work.desc || '',
+
+          tags: [],
+
+          thumbnail: work.coverUrl || '',
+
+          duration: work.duration || 0,
+
+          resolution: detailData?.video?.ratio || '',
+
+          awemeId: work.id,
+
+          authorName: author.nickname || ''
+
+        }).subscribe({
+
+          next: (taskRes: any) => {
+
+            if (!taskRes?.taskId) {
+
+              work.isDownloading = false;
+
+              work.downloadProgress = 0;
+
+              this.showToast('❌ 下载任务创建失败,请稍后重试', 'error');
+
+              return;
+
+            }
+
+
+
+            this.showToast(`📥 正在下载「${work.desc || work.id}」到视频库`, 'info');
+
+            this.pollMonitorWorkDownload(work, taskRes.taskId);
+
+          },
+
+          error: (error: any) => {
+
+            work.isDownloading = false;
+
+            work.downloadProgress = 0;
+
+            this.showToast(`❌ 创建下载任务失败:${error?.error?.error || error?.message || '请稍后重试'}`, 'error');
+
+            this.refreshView();
+
+          }
+
+        });
+
+      },
+
+      error: (error: any) => {
+
+        work.isDownloading = false;
+
+        work.downloadProgress = 0;
+
+        this.showToast(`❌ 获取视频信息失败:${error?.error?.error || error?.message || '请稍后重试'}`, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  getMonitorHeroBackground(author: MonitorAuthor): string {
+
+    return author.coverUrl
+
+      ? `linear-gradient(90deg, rgba(11, 15, 25, 0.92), rgba(11, 15, 25, 0.55)), url(${author.coverUrl})`
+
+      : 'linear-gradient(135deg, #161a27, #232a3d)';
+
+  }
+
+
+
+  // 搜索视频
+
+  searchVideos(): void {
+
+    if (!this.searchKeyword.trim()) return;
+
+
+
+    this.isSearching = true;
+
+    this.searchCursor = 0;
+
+    this.searchId = '';
+
+    this.searchBacktrace = '';
+
+    this.searchResults = [];
+
+    this.filteredSearchResults = [];
+
+
+
+    this.douyinService.searchVideos(this.searchKeyword, '0', 0, '0', '0', '1')
+
+      .subscribe({
+
+        next: (res: any) => {
+
+          this.isSearching = false;
+
+          const parsed = this.parseSearchResponse(res);
+
+          this.searchResults = parsed.items
+
+            .map((item: any) => this.mapVideoItem(item))
+
+            .filter((item: VideoInfo) => !!item.aweme_id || !!item.desc || !!item.cover_url);
+
+          this.applySearchFilters();
+
+          this.hasMore = parsed.hasMore;
+
+          this.searchCursor = parsed.cursor;
+
+          this.searchId = parsed.searchId;
+
+          this.searchBacktrace = parsed.backtrace;
+
+          console.log('🔍 搜索结果解析:', { raw: res, parsed, mapped: this.searchResults });
+
+          this.refreshView();
+
+
+
+          if (this.filteredSearchResults.length === 0) {
+
+            alert('未找到相关视频,请尝试其他关键词');
+
+          }
+
+        },
+
+        error: (error: any) => {
+
+          console.error('搜索失败:', error);
+
+          this.isSearching = false;
+
+          let errorMessage = '搜索失败,请稍后重试';
+
+
+
+          if (error.status === 401) {
+
+            errorMessage = '认证失败,Token可能已失效';
+
+          } else if (error.status === 429) {
+
+            errorMessage = '请求过于频繁,请稍后重试';
+
+          } else if (error.status === 0) {
+
+            errorMessage = '网络连接失败,请检查网络设置';
+
+          }
+
+
+
+          alert(errorMessage);
+
+        }
+
+      });
+
+  }
+
+
+
+  // 映射API返回的视频数据
+
+  private mapVideoItem(item: any): VideoInfo {
+
+    const info = item?.aweme_info || item?.data?.aweme_info || item?.data?.challenge_info || item;
+
+    const coverCandidates = this.buildCoverCandidates(info);
+
+    const coverUrl = coverCandidates[0] || this.getVideoCoverFallback();
+
+    const duration = Math.floor((info.video?.duration || info.duration || 0) / ((info.video?.duration || info.duration || 0) > 1000 ? 1000 : 1));
+
+    const tags = Array.isArray(info.text_extra)
+
+      ? info.text_extra
+
+          .map((tag: any) => tag?.hashtag_name || tag?.cha_name || tag?.tag_name || tag?.hash_tag_name || tag)
+
+          .filter((tag: any) => typeof tag === 'string' && tag.trim())
+
+      : Array.isArray(info.cha_list)
+
+        ? info.cha_list
+
+            .map((tag: any) => tag?.cha_name || tag?.hashtag_name || tag?.tag_name || tag)
+
+            .filter((tag: any) => typeof tag === 'string' && tag.trim())
+
+      : (info.desc ? [info.desc] : []);
+
+
+
+    return {
+
+      aweme_id: info.aweme_id || info.group_id || info.cid || item.data_id || '',
+
+      desc: info.desc || info.cha_name || info.title || info.content || '',
+
+      cover_url: coverUrl,
+
+      cover_candidates: coverCandidates,
+
+      duration: duration,
+
+      create_time: info.create_time || item.create_time || 0,
+
+      publishDate: (info.create_time || item.create_time) ? new Date((info.create_time || item.create_time) * 1000) : null,
+
+      author: {
+
+        nickname: info.author?.nickname || info.nickname || '未知作者',
+
+        sec_uid: info.author?.sec_uid || info.author?.uid || ''
+
+      },
+
+      statistics: {
+
+        play_count: info.statistics?.play_count || info.view_count || 0,
+
+        digg_count: info.statistics?.digg_count || info.user_count || 0,
+
+        comment_count: info.statistics?.comment_count || 0,
+
+        share_count: info.statistics?.share_count || 0
+
+      },
+
+      text_extra: tags
+
+    };
+
+  }
+
+
+
+  private buildCoverCandidates(info: any): string[] {
+
+    const candidates = [
+
+      info.video?.origin_cover?.url_list?.[0],
+
+      info.video?.cover?.url_list?.[0],
+
+      info.video?.dynamic_cover?.url_list?.[0],
+
+      info.video?.animated_cover?.url_list?.[0],
+
+      info.author?.avatar_larger?.url_list?.[0],
+
+      info.author?.avatar_medium?.url_list?.[0],
+
+      info.author?.avatar_thumb?.url_list?.[0],
+
+      info.hashtag_profile,
+
+      info.cover_url
+
+    ];
+
+
+
+    return candidates.filter((url, index, list) => typeof url === 'string' && url.trim() && list.indexOf(url) === index);
+
+  }
+
+
+
+  handleVideoCoverError(video: VideoInfo): void {
+
+    const candidates = video.cover_candidates || [];
+
+    const currentIndex = candidates.indexOf(video.cover_url);
+
+    const nextIndex = currentIndex >= 0 ? currentIndex + 1 : 0;
+
+    const nextCover = candidates[nextIndex];
+
+
+
+    if (nextCover) {
+
+      video.cover_url = nextCover;
+
+    } else {
+
+      video.cover_url = this.getVideoCoverFallback();
+
+    }
+
+
+
+    this.refreshView();
+
+  }
+
+
+
+  private getVideoCoverFallback(): string {
+
+    return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="480" height="270" viewBox="0 0 480 270"><rect width="480" height="270" fill="#f3f4f6"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#9ca3af" font-size="20">封面加载失败</text></svg>');
+
+  }
+
+
+
+  private parseSearchResponse(res: any): { items: any[]; hasMore: boolean; cursor: number; searchId: string; backtrace: string } {
+
+    const normalizedResponse = this.parseJsonPayload(res);
+
+    const normalizedData = this.parseJsonPayload(normalizedResponse?.data);
+
+    const nestedData = this.parseJsonPayload(normalizedData?.data);
+
+    const businessConfig = this.parseJsonPayload(normalizedData?.business_config) || {};
+
+    const nextPage = this.parseJsonPayload(businessConfig?.next_page) || {};
+
+
+
+    const directData = Array.isArray(normalizedData) ? normalizedData : [];
+
+    const nestedArray = Array.isArray(nestedData) ? nestedData : [];
+
+    const businessData = Array.isArray(normalizedData?.business_data) ? normalizedData.business_data : [];
+
+    const items = directData.length > 0 ? directData : (nestedArray.length > 0 ? nestedArray : businessData);
+
+
+
+    const hasMore =
+
+      normalizedResponse?.has_more === 1 ||
+
+      normalizedData?.has_more === 1 ||
+
+      businessConfig?.has_more === 1 ||
+
+      false;
+
+
+
+    const cursor =
+
+      normalizedResponse?.cursor ??
+
+      normalizedData?.cursor ??
+
+      nextPage?.cursor ??
+
+      items.length;
+
+
+
+    const searchId =
+
+      normalizedResponse?.search_id ||
+
+      normalizedData?.search_id ||
+
+      nextPage?.search_id ||
+
+      nextPage?.search_request_id ||
+
+      '';
+
+
+
+    const backtrace =
+
+      normalizedResponse?.backtrace ||
+
+      normalizedData?.backtrace ||
+
+      nextPage?.backtrace ||
+
+      '';
+
+
+
+    return {
+
+      items,
+
+      hasMore,
+
+      cursor: typeof cursor === 'number' ? cursor : Number(cursor || items.length),
+
+      searchId,
+
+      backtrace
+
+    };
+
+  }
+
+
+
+  private parseJsonPayload(value: any): any {
+
+    let current = value;
+
+
+
+    while (typeof current === 'string') {
+
+      const trimmed = current.trim();
+
+      if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
+
+        break;
+
+      }
+
+
+
+      try {
+
+        current = JSON.parse(trimmed);
+
+      } catch {
+
+        break;
+
+      }
+
+    }
+
+
+
+    return current;
+
+  }
+
+
+
+  private normalizeMonitorLabelText(value: any): string {
+
+    const normalized = this.parseJsonPayload(value);
+
+
+
+    if (typeof normalized === 'string') {
+
+      const trimmed = normalized.trim();
+
+      if (!trimmed) {
+
+        return '';
+
+      }
+
+
+
+      const labelTextMatch = trimmed.match(/"label_text"\s*:\s*"([^"]+)"/);
+
+      if (labelTextMatch?.[1]) {
+
+        return labelTextMatch[1].trim();
+
+      }
+
+
+
+      return trimmed;
+
+    }
+
+
+
+    if (Array.isArray(normalized)) {
+
+      return normalized
+
+        .map(item => this.normalizeMonitorLabelText(item))
+
+        .find(item => !!item) || '';
+
+    }
+
+
+
+    if (normalized && typeof normalized === 'object') {
+
+      const candidateKeys = ['label_text', 'text', 'name', 'title', 'desc'];
+
+      for (const key of candidateKeys) {
+
+        const text = this.normalizeMonitorLabelText(normalized[key]);
+
+        if (text) {
+
+          return text;
+
+        }
+
+      }
+
+    }
+
+
+
+    return '';
+
+  }
+
+
+
+  getMonitorIpLocationText(author: MonitorAuthor | null | undefined): string {
+
+    const location = this.normalizeMonitorIpLocation(author?.ipLocation);
+
+    return location ? `IP 属地:${location}` : 'IP 属地未抓取';
+
+  }
+
+
+
+  private normalizeMonitorIpLocation(value: any): string {
+
+    const normalized = this.normalizeMonitorLabelText(value);
+
+    if (!normalized) {
+
+      return '';
+
+    }
+
+
+
+    return normalized.replace(/^IP\s*属地\s*[::]?\s*/i, '').trim();
+
+  }
+
+
+
+  private pickMonitorCountValue(values: any[]): number {
+
+    for (const value of values) {
+
+      const parsed = this.parseMonitorCountValue(value);
+
+      if (parsed !== null) {
+
+        return parsed;
+
+      }
+
+    }
+
+
+
+    return 0;
+
+  }
+
+
+
+  private parseMonitorCountValue(value: any): number | null {
+
+    const normalized = this.parseJsonPayload(value);
+
+
+
+    if (typeof normalized === 'number') {
+
+      return Number.isFinite(normalized) ? normalized : null;
+
+    }
+
+
+
+    if (typeof normalized === 'string') {
+
+      const trimmed = normalized.trim();
+
+      if (!trimmed) {
+
+        return null;
+
+      }
+
+
+
+      const compact = trimmed.replace(/,/g, '').replace(/\s+/g, '');
+
+      const unitMatch = compact.match(/^([\d.]+)(亿|万|[kKmMwW])$/);
+
+      if (unitMatch) {
+
+        const base = Number(unitMatch[1]);
+
+        if (!Number.isFinite(base)) {
+
+          return null;
+
+        }
+
+
+
+        const unit = unitMatch[2].toLowerCase();
+
+        const multiplier = unit === '亿'
+
+          ? 100000000
+
+          : (unit === '万' || unit === 'w')
+
+            ? 10000
+
+            : unit === 'm'
+
+              ? 1000000
+
+              : 1000;
+
+        return Math.round(base * multiplier);
+
+      }
+
+
+
+      const directNumber = Number(compact);
+
+      return Number.isFinite(directNumber) ? directNumber : null;
+
+    }
+
+
+
+    if (normalized && typeof normalized === 'object') {
+
+      const candidateKeys = ['count', 'value', 'total', 'num'];
+
+      for (const key of candidateKeys) {
+
+        const parsed = this.parseMonitorCountValue(normalized[key]);
+
+        if (parsed !== null) {
+
+          return parsed;
+
+        }
+
+      }
+
+    }
+
+
+
+    return null;
+
+  }
+
+
+
+  onSearchFilterChange(): void {
+
+    this.applySearchFilters();
+
+    this.refreshView();
+
+  }
+
+
+
+  private applySearchFilters(): void {
+
+    let items = [...this.searchResults];
+
+
+
+    if (this.publishTime !== '0') {
+
+      const nowSeconds = Math.floor(Date.now() / 1000);
+
+      const limitSeconds = Number(this.publishTime) * 24 * 60 * 60;
+
+      items = items.filter((video) => !video.create_time || (nowSeconds - video.create_time) <= limitSeconds);
+
+    }
+
+
+
+    if (this.filterDuration !== '0') {
+
+      const [minMinutes, maxMinutes] = this.filterDuration.split('-').map(value => Number(value || 0));
+
+      items = items.filter((video) => {
+
+        const durationMinutes = (video.duration || 0) / 60;
+
+        return durationMinutes >= minMinutes && durationMinutes <= maxMinutes;
+
+      });
+
+    }
+
+
+
+    if (this.sortType === '1') {
+
+      items.sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0));
+
+    } else if (this.sortType === '2') {
+
+      items.sort((a, b) => (b.create_time || 0) - (a.create_time || 0));
+
+    }
+
+
+
+    this.filteredSearchResults = items;
+
+  }
+
+
+
+  private fetchMonitorAuthorWorks(author: MonitorAuthor, append: boolean): void {
+
+    if (!author.secUserId || author.loadingWorks) {
+
+      return;
+
+    }
+
+
+
+    author.loadingWorks = true;
+
+    const cursor = append ? author.worksCursor : '0';
+
+    this.refreshView();
+
+
+
+    this.douyinService.getUserPostVideos(author.secUserId, cursor).subscribe({
+
+      next: (res: any) => {
+
+        const parsed = this.douyinService.extractUserPostVideos(res);
+
+        const mappedWorks = parsed.items
+
+          .map((item: any) => this.mapMonitorWork(item))
+
+          .filter((item: MonitorWork) => !!item.id || !!item.desc || !!item.coverUrl);
+
+
+
+        author.works = append
+
+          ? [...author.works, ...mappedWorks.filter(item => !author.works.some(existing => existing.id === item.id))]
+
+          : mappedWorks;
+
+        author.worksCursor = parsed.cursor;
+
+        author.hasMoreWorks = parsed.hasMore;
+
+        author.loadingWorks = false;
+
+        author.coverUrl = author.coverUrl || author.works[0]?.coverUrl || author.avatarUrl;
+
+        this.persistMonitorState();
+
+        this.refreshView();
+
+      },
+
+      error: (error: any) => {
+
+        author.loadingWorks = false;
+
+        this.showToast(`❌ ${this.getErrorText(error, '加载博主作品失败,请稍后重试')}`, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  private mapMonitorAuthor(profile: any, uid: string): MonitorAuthor {
+
+    const avatarCandidates = this.pickUniqueStringValues([
+
+      profile?.avatar_larger?.url_list,
+
+      profile?.avatar_medium?.url_list,
+
+      profile?.avatar_thumb?.url_list,
+
+      profile?.avatar_168x168?.url_list,
+
+      profile?.avatar_url
+
+    ]);
+
+    const coverCandidates = this.pickUniqueStringValues([
+
+      profile?.cover_url,
+
+      profile?.cover_urls,
+
+      profile?.cover_thumb?.url_list,
+
+      profile?.video_cover?.url_list,
+
+      avatarCandidates
+
+    ]);
+
+    const nickname = profile?.nickname || profile?.display_name || `UID ${uid}`;
+
+    const verifiedText = [
+
+      profile?.custom_verify,
+
+      profile?.enterprise_verify_reason,
+
+      profile?.account_cert_info,
+
+      profile?.verification_info
+
+    ].map((item: any) => this.normalizeMonitorLabelText(item)).find((item: string) => !!item) || '';
+
+    const ipLocation = this.normalizeMonitorIpLocation(
+
+      [
+
+        profile?.ip_location,
+
+        profile?.ip_label,
+
+        profile?.ipLocation,
+
+        profile?.ip_info?.location,
+
+        profile?.ip_info?.ip_location,
+
+        profile?.ip_region,
+
+        profile?.location
+
+      ].find((item: any) => !!this.normalizeMonitorLabelText(item)) || ''
+
+    );
+
+    const followerCount = this.pickMonitorCountValue([
+
+      profile?.follower_count,
+
+      profile?.followerCount,
+
+      profile?.fans_count,
+
+      profile?.fansCount,
+
+      profile?.public_fans_count,
+
+      profile?.statistics?.follower_count,
+
+      profile?.statistics?.fans_count,
+
+      profile?.author_stats?.follower_count,
+
+      profile?.author_stats?.fans_count,
+
+      profile?.user_stats?.follower_count,
+
+      profile?.user_stats?.fans_count,
+
+      profile?.user_statistics?.follower_count,
+
+      profile?.user_statistics?.fans_count,
+
+      profile?.stats?.follower_count,
+
+      profile?.stats?.fans_count
+
+    ]);
+
+    const followingCount = this.pickMonitorCountValue([
+
+      profile?.following_count,
+
+      profile?.followingCount,
+
+      profile?.statistics?.following_count,
+
+      profile?.author_stats?.following_count,
+
+      profile?.user_stats?.following_count,
+
+      profile?.user_statistics?.following_count,
+
+      profile?.stats?.following_count
+
+    ]);
+
+    const totalFavorited = this.pickMonitorCountValue([
+
+      profile?.total_favorited,
+
+      profile?.favoriting_count,
+
+      profile?.totalFavorited,
+
+      profile?.liked_count,
+
+      profile?.statistics?.total_favorited,
+
+      profile?.statistics?.favoriting_count,
+
+      profile?.author_stats?.total_favorited,
+
+      profile?.user_stats?.total_favorited,
+
+      profile?.user_statistics?.total_favorited,
+
+      profile?.stats?.total_favorited
+
+    ]);
+
+    const awemeCount = this.pickMonitorCountValue([
+
+      profile?.aweme_count,
+
+      profile?.video_count,
+
+      profile?.awemeCount,
+
+      profile?.statistics?.aweme_count,
+
+      profile?.statistics?.video_count,
+
+      profile?.author_stats?.aweme_count,
+
+      profile?.user_stats?.aweme_count,
+
+      profile?.user_statistics?.aweme_count,
+
+      profile?.stats?.aweme_count
+
+    ]);
+
+
+
+    return {
+
+      id: String(profile?.uid || uid),
+
+      uid: String(uid),
+
+      secUserId: String(profile?.sec_uid || profile?.sec_user_id || profile?.secUid || ''),
+
+      nickname,
+
+      uniqueId: String(profile?.unique_id || profile?.short_id || profile?.douyin_id || ''),
+
+      shortId: String(profile?.short_id || profile?.douyin_id || ''),
+
+      signature: profile?.signature || profile?.bio_description || '',
+
+      ipLocation,
+
+      verifiedText,
+
+      avatarUrl: avatarCandidates[0] || this.getMonitorAvatarFallback(nickname),
+
+      avatarCandidates,
+
+      coverUrl: coverCandidates[0] || '',
+
+      coverCandidates,
+
+      followerCount,
+
+      followingCount,
+
+      totalFavorited,
+
+      awemeCount,
+
+      works: [],
+
+      worksCursor: '0',
+
+      hasMoreWorks: false,
+
+      loadingWorks: false
+
+    };
+
+  }
+
+
+
+  private mapMonitorWork(item: any): MonitorWork {
+
+    const coverCandidates = this.pickUniqueStringValues([
+
+      item?.video?.origin_cover?.url_list,
+
+      item?.video?.cover?.url_list,
+
+      item?.video?.dynamic_cover?.url_list,
+
+      item?.cover?.url_list,
+
+      item?.images?.map((image: any) => image?.url_list?.[0] || image?.download_url_list?.[0]),
+
+      item?.image_infos?.map((image: any) => image?.label_large?.url_list?.[0] || image?.label_thumb?.url_list?.[0]),
+
+      item?.cover_url
+
+    ]);
+
+    const rawDuration = Number(item?.video?.duration || item?.duration || 0);
+
+    const createTimestamp = Number(item?.create_time || 0);
+
+    const statistics = item?.statistics || item?.statis || {};
+
+    const hasImageGroup = (Array.isArray(item?.images) && item.images.length > 0) || (Array.isArray(item?.image_infos) && item.image_infos.length > 0);
+
+
+
+    return {
+
+      id: String(item?.aweme_id || item?.group_id || item?.item_id || `${Date.now()}-${Math.random()}`),
+
+      desc: item?.desc || item?.title || '未命名作品',
+
+      coverUrl: coverCandidates[0] || this.getVideoCoverFallback(),
+
+      coverCandidates,
+
+      duration: Math.floor(rawDuration / (rawDuration > 1000 ? 1000 : 1)),
+
+      createTime: createTimestamp ? new Date(createTimestamp > 1000000000000 ? createTimestamp : createTimestamp * 1000) : undefined,
+
+      type: hasImageGroup ? 'image' : 'video',
+
+      statistics: {
+
+        play_count: Number(statistics?.play_count || statistics?.playCount || 0),
+
+        digg_count: Number(statistics?.digg_count || 0),
+
+        comment_count: Number(statistics?.comment_count || 0),
+
+        share_count: Number(statistics?.share_count || 0)
+
+      }
+
+    };
+
+  }
+
+
+
+  private pickUniqueStringValues(values: any[]): string[] {
+
+    const uniqueValues: string[] = [];
+
+
+
+    const collect = (value: any) => {
+
+      if (Array.isArray(value)) {
+
+        value.forEach(collect);
+
+        return;
+
+      }
+
+
+
+      if (typeof value === 'string' && value.trim() && !uniqueValues.includes(value)) {
+
+        uniqueValues.push(value);
+
+      }
+
+    };
+
+
+
+    values.forEach(collect);
+
+    return uniqueValues;
+
+  }
+
+
+
+  private getMonitorAvatarFallback(name: string): string {
+
+    const label = (name || '博主').trim().slice(0, 2) || '博主';
+
+    return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160"><rect width="160" height="160" rx="80" fill="#ff0050"/><text x="50%" y="54%" text-anchor="middle" dominant-baseline="middle" fill="#ffffff" font-size="48" font-family="Arial, sans-serif">${label}</text></svg>`);
+
+  }
+
+
+
+  private getErrorText(error: any, fallback: string): string {
+
+    return error?.error?.error || error?.error?.msg || error?.message || fallback;
+
+  }
+
+
+
+  private async extractHttpBlobErrorMessage(error: any, fallback: string): Promise<string> {
+
+    if (error instanceof HttpErrorResponse && error.error instanceof Blob) {
+
+      try {
+
+        const text = await error.error.text();
+
+        if (text) {
+
+          const parsed = JSON.parse(text);
+
+          return parsed?.error || parsed?.message || fallback;
+
+        }
+
+      } catch {
+
+        return fallback;
+
+      }
+
+    }
+
+
+
+    return error?.error?.error || error?.error?.message || error?.message || fallback;
+
+  }
+
+
+
+  private serializeMonitorAuthor(author: MonitorAuthor): any {
+
+    return {
+
+      ...author,
+
+      loadingWorks: false,
+
+      works: author.works.map(work => ({
+
+        ...work,
+
+        createTime: work.createTime ? work.createTime.toISOString() : null,
+
+        isDownloading: false,
+
+        downloadProgress: 0
+
+      }))
+
+    };
+
+  }
+
+
+
+  private deserializeMonitorAuthor(author: any): MonitorAuthor {
+
+    return {
+
+      id: String(author?.id || ''),
+
+      uid: String(author?.uid || ''),
+
+      secUserId: String(author?.secUserId || ''),
+
+      nickname: String(author?.nickname || ''),
+
+      uniqueId: String(author?.uniqueId || ''),
+
+      shortId: String(author?.shortId || ''),
+
+      signature: String(author?.signature || ''),
+
+      ipLocation: String(author?.ipLocation || ''),
+
+      verifiedText: this.normalizeMonitorLabelText(author?.verifiedText),
+
+      avatarUrl: String(author?.avatarUrl || this.getMonitorAvatarFallback(author?.nickname || '博主')),
+
+      avatarCandidates: Array.isArray(author?.avatarCandidates) ? author.avatarCandidates : [],
+
+      coverUrl: String(author?.coverUrl || ''),
+
+      coverCandidates: Array.isArray(author?.coverCandidates) ? author.coverCandidates : [],
+
+      followerCount: Number(author?.followerCount || 0),
+
+      followingCount: Number(author?.followingCount || 0),
+
+      totalFavorited: Number(author?.totalFavorited || 0),
+
+      awemeCount: Number(author?.awemeCount || 0),
+
+      works: Array.isArray(author?.works)
+
+        ? author.works.map((work: any) => ({
+
+            id: String(work?.id || ''),
+
+            desc: String(work?.desc || ''),
+
+            coverUrl: String(work?.coverUrl || this.getVideoCoverFallback()),
+
+            coverCandidates: Array.isArray(work?.coverCandidates) ? work.coverCandidates : [],
+
+            duration: Number(work?.duration || 0),
+
+            createTime: work?.createTime ? new Date(work.createTime) : undefined,
+
+            type: work?.type === 'image' ? 'image' : 'video',
+
+            statistics: {
+
+              play_count: Number(work?.statistics?.play_count || 0),
+
+              digg_count: Number(work?.statistics?.digg_count || 0),
+
+              comment_count: Number(work?.statistics?.comment_count || 0),
+
+              share_count: Number(work?.statistics?.share_count || 0)
+
+            },
+
+            isDownloading: false,
+
+            downloadProgress: 0
+
+          }))
+
+        : [],
+
+      worksCursor: String(author?.worksCursor || '0'),
+
+      hasMoreWorks: !!author?.hasMoreWorks,
+
+      loadingWorks: false
+
+    };
+
+  }
+
+
+
+  // 加载更多搜索结果
+
+  loadMore(): void {
+
+    if (!this.hasMore || this.isSearching) return;
+
+
+
+    this.isSearching = true;
+
+    this.douyinService.searchVideos(
+
+      this.searchKeyword, '0', this.searchCursor, '0',
+
+      '0', '1', this.searchId, this.searchBacktrace
+
+    ).subscribe({
+
+      next: (res: any) => {
+
+        this.isSearching = false;
+
+        const parsed = this.parseSearchResponse(res);
+
+        const newItems = parsed.items
+
+          .map((item: any) => this.mapVideoItem(item))
+
+          .filter((item: VideoInfo) => !!item.aweme_id || !!item.desc || !!item.cover_url);
+
+        this.searchResults.push(...newItems);
+
+        this.applySearchFilters();
+
+        this.hasMore = parsed.hasMore;
+
+        this.searchCursor = parsed.cursor;
+
+        this.searchId = parsed.searchId || this.searchId;
+
+        this.searchBacktrace = parsed.backtrace || this.searchBacktrace;
+
+        this.refreshView();
+
+      },
+
+      error: (error: any) => {
+
+        console.error('加载更多失败:', error);
+
+        this.isSearching = false;
+
+        let errorMessage = '加载更多失败,请稍后重试';
+
+
+
+        if (error.status === 401) {
+
+          errorMessage = '认证失败,Token可能已失效';
+
+        } else if (error.status === 429) {
+
+          errorMessage = '请求过于频繁,请稍后重试';
+
+        } else if (error.status === 0) {
+
+          errorMessage = '网络连接失败,请检查网络设置';
+
+        }
+
+
+
+        alert(errorMessage);
+
+      }
+
+    });
+
+  }
+
+
+
+  // 查看视频详情
+
+  viewVideoDetail(video: VideoInfo): void {
+
+    this.openSearchVideoPreview(video, null, '', true);
+
+    this.douyinService.getVideoDetail(video.aweme_id).subscribe({
+
+      next: (res: any) => {
+
+        const detailData = this.douyinService.extractVideoDetailData(res) || {};
+
+        this.openSearchVideoPreview(video, detailData, this.douyinService.extractVideoPreviewUrl(res) || '', false);
+
+      },
+
+      error: (error: any) => {
+
+        console.error('获取视频详情失败:', error);
+
+        this.openSearchVideoPreview(video, null, '', false);
+
+        this.showToast('⚠️ 获取实时详情失败,已展示基础信息', 'warn');
+
+      }
+
+    });
+
+  }
+
+
+
+  private openSearchVideoPreview(video: VideoInfo, detailData: any, previewUrl: string, loading: boolean = false): void {
+
+    const info = detailData || {};
+
+    const tags = this.extractVideoTags(info, video.text_extra || []);
+
+    const rawDuration = info.video?.duration || info.duration || video.duration || 0;
+
+    const duration = Math.floor(rawDuration / (rawDuration > 1000 ? 1000 : 1));
+
+    const coverCandidates = detailData ? this.buildCoverCandidates(info) : [];
+
+    const poster = coverCandidates[0] || video.cover_url || this.getVideoCoverFallback();
+
+    const createTime = info.create_time ? new Date(info.create_time * 1000) : undefined;
+
+    const filename = this.buildVideoFilename(video);
+
+
+
+    this.previewVideoUrl = previewUrl ? this.buildVideoProxyUrl(previewUrl, filename) : '';
+
+    this.previewDownloadUrl = '';
+
+    this.previewVideoTitle = info.desc || video.desc || video.aweme_id;
+
+    this.previewSourceVideo = video;
+
+    this.previewVideoInfo = {
+
+      duration: duration || video.duration || 0,
+
+      resolution: info.video?.ratio || '',
+
+      format: 'mp4',
+
+      source: 'douyin',
+
+      description: info.desc || video.desc || '',
+
+      tags,
+
+      created_at: createTime,
+
+      poster,
+
+      authorName: info.author?.nickname || video.author.nickname || '',
+
+      playCount: info.statistics?.play_count ?? video.statistics.play_count,
+
+      diggCount: info.statistics?.digg_count ?? video.statistics.digg_count,
+
+      commentCount: info.statistics?.comment_count ?? video.statistics.comment_count,
+
+      shareCount: info.statistics?.share_count ?? video.statistics.share_count,
+
+      loading
+
+    };
+
+    this.showPreviewModal = true;
+
+    this.refreshView();
+
+  }
+
+
+
+  private extractVideoTags(info: any, fallbackTags: string[] = []): string[] {
+
+    const textExtraTags = Array.isArray(info?.text_extra)
+
+      ? info.text_extra
+
+          .map((tag: any) => tag?.hashtag_name || tag?.cha_name || tag?.tag_name || tag?.hash_tag_name || tag)
+
+          .filter((tag: any) => typeof tag === 'string' && tag.trim())
+
+      : [];
+
+
+
+    const chaListTags = Array.isArray(info?.cha_list)
+
+      ? info.cha_list
+
+          .map((tag: any) => tag?.cha_name || tag?.hashtag_name || tag?.tag_name || tag)
+
+          .filter((tag: any) => typeof tag === 'string' && tag.trim())
+
+      : [];
+
+
+
+    return [...textExtraTags, ...chaListTags, ...fallbackTags].filter((tag, index, list) => !!tag && list.indexOf(tag) === index);
+
+  }
+
+
+
+  private buildVideoFilename(video: VideoInfo): string {
+
+    return `${video.author.nickname}_${video.desc || video.aweme_id}`
+
+      .replace(/[<>:"/\\|?*]/g, '_')
+
+      .substring(0, 60) + '.mp4';
+
+  }
+
+
+
+  private buildVideoProxyUrl(videoUrl: string, filename: string, download: boolean = false): string {
+
+    const params = new URLSearchParams({
+
+      url: videoUrl,
+
+      filename,
+
+      download: download ? '1' : '0'
+
+    });
+
+
+
+    return `/backend/api/video-proxy?${params.toString()}`;
+
+  }
+
+
+
+  // 下载抖音视频到本地
+
+  downloadVideo(video: VideoInfo): void {
+
+    if (video.isDownloading) return;
+
+
+
+    video.isDownloading = true;
+
+    video.downloadProgress = 0;
+
+
+
+    // 先获取视频详情拿到下载地址
+
+    this.douyinService.getVideoDetail(video.aweme_id).subscribe({
+
+      next: (res: any) => {
+
+        const detailData = this.douyinService.extractVideoDetailData(res) || {};
+
+        const downloadUrls = this.douyinService.extractVideoDownloadUrls(res);
+
+        const downloadUrl = downloadUrls[0] || '';
+
+        if (!downloadUrl) {
+
+          console.warn('未找到可用下载地址:', detailData);
+
+          video.isDownloading = false;
+
+          this.showToast('❌ 无法获取下载地址,请稍后重试', 'error');
+
+          return;
+
+        }
+
+
+
+        const filename = this.buildVideoFilename(video);
+
+        this.douyinService.startManagedDownload({
+
+          url: downloadUrl,
+
+          urls: downloadUrls,
+
+          filename,
+
+          title: video.desc || video.aweme_id,
+
+          description: detailData?.desc || video.desc || '',
+
+          tags: video.text_extra || [],
+
+          thumbnail: video.cover_url || '',
+
+          duration: video.duration || 0,
+
+          resolution: detailData?.video?.ratio || '',
+
+          awemeId: video.aweme_id,
+
+          authorName: video.author.nickname || ''
+
+        }).subscribe({
+
+          next: (taskRes: any) => {
+
+            if (!taskRes?.taskId) {
+
+              video.isDownloading = false;
+
+              video.downloadProgress = 0;
+
+              this.showToast('❌ 下载任务创建失败,请稍后重试', 'error');
+
+              return;
+
+            }
+
+
+
+            this.showToast(`📥 正在下载「${video.desc || video.aweme_id}」到视频库`, 'info');
+
+            this.pollManagedDownload(video, taskRes.taskId);
+
+          },
+
+          error: (error: any) => {
+
+            console.error('创建下载任务失败:', error);
+
+            video.isDownloading = false;
+
+            video.downloadProgress = 0;
+
+            this.showToast(`❌ 创建下载任务失败:${error?.error?.error || error?.message || '请稍后重试'}`, 'error');
+
+          }
+
+        });
+
+      },
+
+      error: (error: any) => {
+
+        console.error('获取视频信息失败:', error);
+
+        video.isDownloading = false;
+
+        this.showToast(`❌ 获取视频信息失败:${error?.error?.error || error?.message || '请稍后重试'}`, 'error');
+
+      }
+
+    });
+
+  }
+
+
+
+  downloadPreviewVideo(): void {
+
+    if (!this.previewSourceVideo) {
+
+      return;
+
+    }
+
+
+
+    this.downloadVideo(this.previewSourceVideo);
+
+  }
+
+
+
+  private pollManagedDownload(video: VideoInfo, taskId: string, retryCount: number = 0): void {
+
+    this.douyinService.getManagedDownloadTask(taskId).subscribe({
+
+      next: (task: any) => {
+
+        video.downloadProgress = task?.progress || 0;
+
+
+
+        if (task?.status === 'completed') {
+
+          video.isDownloading = false;
+
+          video.downloadProgress = 100;
+
+          this.loadVideoManifest();
+
+          this.showToast(`✅ 视频「${video.desc || video.aweme_id}」已下载到本地视频库`, 'success');
+
+          this.refreshView();
+
+          return;
+
+        }
+
+
+
+        if (task?.status === 'failed') {
+
+          video.isDownloading = false;
+
+          video.downloadProgress = 0;
+
+          this.showToast(`❌ 下载失败:${task?.error || '请稍后重试'}`, 'error');
+
+          this.refreshView();
+
+          return;
+
+        }
+
+
+
+        this.refreshView();
+
+        setTimeout(() => this.pollManagedDownload(video, taskId), 1000);
+
+      },
+
+      error: (error: any) => {
+
+        console.error('查询下载任务失败:', error);
+
+        if (retryCount < 3) {
+
+          setTimeout(() => this.pollManagedDownload(video, taskId, retryCount + 1), 1500);
+
+          return;
+
+        }
+
+
+
+        video.isDownloading = false;
+
+        video.downloadProgress = 0;
+
+        this.showToast('❌ 下载状态查询失败,请稍后重试', 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  private pollMonitorWorkDownload(work: MonitorWork, taskId: string, retryCount: number = 0): void {
+
+    this.douyinService.getManagedDownloadTask(taskId).subscribe({
+
+      next: (task: any) => {
+
+        work.downloadProgress = task?.progress || 0;
+
+
+
+        if (task?.status === 'completed') {
+
+          work.isDownloading = false;
+
+          work.downloadProgress = 100;
+
+          this.loadVideoManifest();
+
+          this.showToast(`✅ 视频「${work.desc || work.id}」已下载到本地视频库`, 'success');
+
+          this.refreshView();
+
+          return;
+
+        }
+
+
+
+        if (task?.status === 'failed') {
+
+          work.isDownloading = false;
+
+          work.downloadProgress = 0;
+
+          this.showToast(`❌ 下载失败:${task?.error || '请稍后重试'}`, 'error');
+
+          this.refreshView();
+
+          return;
+
+        }
+
+
+
+        this.refreshView();
+
+        setTimeout(() => this.pollMonitorWorkDownload(work, taskId), 1000);
+
+      },
+
+      error: () => {
+
+        if (retryCount < 3) {
+
+          setTimeout(() => this.pollMonitorWorkDownload(work, taskId, retryCount + 1), 1500);
+
+          return;
+
+        }
+
+
+
+        work.isDownloading = false;
+
+        work.downloadProgress = 0;
+
+        this.showToast('❌ 下载状态查询失败,请稍后重试', 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  private buildMonitorVideoInfo(author: MonitorAuthor, work: MonitorWork): VideoInfo {
+
+    return {
+
+      aweme_id: work.id,
+
+      desc: work.desc,
+
+      cover_url: work.coverUrl,
+
+      cover_candidates: work.coverCandidates,
+
+      duration: work.duration,
+
+      create_time: work.createTime ? Math.floor(work.createTime.getTime() / 1000) : 0,
+
+      author: {
+
+        nickname: author.nickname,
+
+        sec_uid: author.secUserId
+
+      },
+
+      statistics: {
+
+        play_count: 0,
+
+        digg_count: work.statistics.digg_count,
+
+        comment_count: work.statistics.comment_count,
+
+        share_count: work.statistics.share_count
+
+      },
+
+      text_extra: [],
+
+      downloadProgress: work.downloadProgress,
+
+      isDownloading: work.isDownloading
+
+    };
+
+  }
+
+
+
+  // 开始AI生成
+
+  startAIGeneration(video: VideoInfo): void {
+
+    console.log('开始AI生成:', video);
+
+
+
+    // 创建新任务
+
+    const newTask: Task = {
+
+      id: `TASK-${Date.now()}`,
+
+      keyword: this.searchKeyword,
+
+      status: 'pending',
+
+      step: 'search',
+
+      progress: 0,
+
+      cost: 0,
+
+      created_at: new Date(),
+
+      updated_at: new Date(),
+
+      original_video_title: video.desc
+
+    };
+
+
+
+    this.activeTasks.unshift(newTask);
+
+    this.setCurrentTab('tasks');
+
+
+
+    // 模拟任务进度更新
+
+    this.simulateTaskProgress(newTask.id);
+
+  }
+
+
+
+  // 模拟任务进度
+
+  private simulateTaskProgress(taskId: string): void {
+
+    const task = this.activeTasks.find(t => t.id === taskId);
+
+    if (!task) return;
+
+
+
+    const steps: Task['step'][] = ['search', 'detail', 'download', 'transcribe', 'generate'];
+
+    let currentStepIndex = 0;
+
+
+
+    const interval = setInterval(() => {
+
+      if (currentStepIndex < steps.length) {
+
+        task.step = steps[currentStepIndex];
+
+        task.progress = Math.min((currentStepIndex + 1) * 20, 100);
+
+        task.status = currentStepIndex === steps.length - 1 ? 'completed' : 'processing';
+
+        task.updated_at = new Date();
+
+
+
+        currentStepIndex++;
+
+      } else {
+
+        task.status = 'completed';
+
+        task.progress = 100;
+
+        clearInterval(interval);
+
+
+
+        // 添加到生成结果
+
+        this.generatedResults.unshift({
+
+          id: `RESULT-${Date.now()}`,
+
+          type: 'video',
+
+          url: 'https://example.com/generated-video.mp4',
+
+          title: `AI重塑 - ${task.keyword}`,
+
+          created_at: new Date(),
+
+          quality: '1080P',
+
+          duration: '2分钟',
+
+          cost: 2.5
+
+        });
+
+      }
+
+    }, 2000);
+
+  }
+
+
+
+  // 获取任务状态颜色
+
+  getTaskStatusColor(status: string): string {
+
+    switch (status) {
+
+      case 'pending': return 'warn';
+
+      case 'processing': return 'accent';
+
+      case 'completed': return 'primary';
+
+      case 'failed': return 'warn';
+
+      default: return '';
+
+    }
+
+  }
+
+
+
+  // 获取任务状态文本
+
+  getTaskStatusText(status: string): string {
+
+    switch (status) {
+
+      case 'pending': return '等待中';
+
+      case 'processing': return '处理中';
+
+      case 'completed': return '已完成';
+
+      case 'failed': return '失败';
+
+      case 'timeout': return '超时';
+
+      default: return status;
+
+    }
+
+  }
+
+
+
+  // 获取任务步骤文本
+
+  getTaskStepText(step: string): string {
+
+    switch (step) {
+
+      case 'search': return '视频搜索';
+
+      case 'detail': return '详情获取';
+
+      case 'download': return '视频下载';
+
+      case 'transcribe': return '语音转文字';
+
+      case 'generate': return 'AI生成';
+
+      case 'complete': return '完成';
+
+      case 'vg-upload': return '上传视频';
+
+      case 'vg-analyze': return '视频解析';
+
+      case 'vg-script': return '脚本生成';
+
+      case 'vg-image': return '配图生成';
+
+      case 'vg-voice': return '语音配音';
+
+      case 'vg-composite': return '视频合成';
+
+      default: return step;
+
+    }
+
+  }
+
+
+
+  // 判断步骤是否完成
+
+  isStepCompleted(currentStep: string, targetStep: string): boolean {
+
+    const steps = ['search', 'detail', 'download', 'transcribe', 'generate'];
+
+    const currentIndex = steps.indexOf(currentStep);
+
+    const targetIndex = steps.indexOf(targetStep);
+
+    return currentIndex > targetIndex;
+
+  }
+
+
+
+  // 查看任务详情
+
+  viewTaskDetail(task: Task): void {
+
+    console.log('查看任务详情:', task);
+
+  }
+
+
+
+  // 已被用户取消的任务 ID 集合:用于中止后续 vg 流程更新进度时覆盖回 processing 状态
+
+  private canceledTaskIds: Set<string> = new Set<string>();
+
+
+
+  // 取消任务(实际生效:标记为 failed 并阻止后续 vg 进度回写;UI 立即刷新)
+
+  cancelTask(task: Task): void {
+
+    if (!task) return;
+
+    task.status = 'failed';
+
+    task.error_message = '用户已取消';
+
+    task.updated_at = new Date();
+
+    if (task.id) this.canceledTaskIds.add(task.id);
+
+    // 若取消的是当前进行中的视频生成任务,清空 vg 运行状态,防止继续触发新请求
+
+    if (task.type === 'video-generation' && this.vgTaskId === task.id) {
+
+      this.vgUploading = false;
+
+      this.vgAnalyzing = false;
+
+      this.vgGeneratingScript = false;
+
+      this.vgGeneratingImages = false;
+
+      this.vgSynthesizing = false;
+
+      this.vgCompositing = false;
+
+      this.vgError = '已取消';
+
+    }
+
+    this.updateTask(task);
+
+    this.updateTaskStats();
+
+    this.refreshView();
+
+    this.showToast('🛑 任务已取消', 'info');
+
+  }
+
+
+
+  // 获取状态颜色
+
+  getStatusColor(status: string): string {
+
+    switch (status) {
+
+      case 'completed': return 'primary';
+
+      case 'failed': return 'warn';
+
+      default: return '';
+
+    }
+
+  }
+
+
+
+  // 获取状态文本
+
+  getStatusText(status: string): string {
+
+    switch (status) {
+
+      case 'completed': return '已完成';
+
+      case 'failed': return '失败';
+
+      default: return status;
+
+    }
+
+  }
+
+
+
+  // 查看历史记录详情
+
+  viewHistoryDetail(record: HistoryRecord): void {
+
+    this.historyDetailRecord = record;
+
+    this.historyDetailSegments = [];
+
+    this.historyDetailPrimaryVideoUrl = this.findHistoryPrimaryVideoUrl(record);
+
+    this.historyDetailLoading = !!record.remixId;
+
+    this.showHistoryDetailModal = true;
+
+    this.refreshView();
+
+
+
+    const finalizeHistoryDetail = (remix: any = null) => {
+
+      this.historyDetailSegments = this.buildHistoryDetailSegments(record, remix);
+
+      this.historyDetailPrimaryVideoUrl = this.findHistoryPrimaryVideoUrl(record, remix);
+
+      this.historyDetailLoading = false;
+
+      this.refreshView();
+
+    };
+
+
+
+    if (record.remixId && record.videoId) {
+
+      // 从 remix 数据加载片段详情
+
+      this.http.get<any[]>(`/backend/api/remixes/${record.videoId}`).subscribe({
+
+        next: (remixes) => {
+
+          finalizeHistoryDetail(remixes.find(r => r.remixId === record.remixId));
+
+        },
+
+        error: () => {
+
+          finalizeHistoryDetail();
+
+        }
+
+      });
+
+    } else if (record.remixId) {
+
+      // 尝试从所有 remix 数据中查找
+
+      this.http.get<Record<string, any[]>>('/backend/api/remixes').subscribe({
+
+        next: (allRemixes) => {
+
+          let matchedRemix: any = null;
+
+          for (const remixes of Object.values(allRemixes)) {
+
+            const remix = remixes.find(r => r.remixId === record.remixId);
+
+            if (remix) {
+
+              matchedRemix = remix;
+
+              break;
+
+            }
+
+          }
+
+          finalizeHistoryDetail(matchedRemix);
+
+        },
+
+        error: () => {
+
+          finalizeHistoryDetail();
+
+        }
+
+      });
+
+    } else {
+
+      finalizeHistoryDetail();
+
+    }
+
+  }
+
+
+
+  closeHistoryDetail(): void {
+
+    this.showHistoryDetailModal = false;
+
+    this.historyDetailRecord = null;
+
+    this.historyDetailSegments = [];
+
+    this.historyDetailPrimaryVideoUrl = '';
+
+    this.historyDetailLoading = false;
+
+  }
+
+
+
+  private findGeneratedResultForHistory(record: HistoryRecord): GeneratedResult | undefined {
+
+    return this.generatedResults.find((result) => {
+
+      if (record.remixId && result.remixId === record.remixId) return true;
+
+      if (record.resultUrl && result.url === record.resultUrl) return true;
+
+      if (record.videoId && result.videoId === record.videoId && record.styleName) {
+
+        return result.title?.includes(record.styleName);
+
+      }
+
+      return false;
+
+    });
+
+  }
+
+
+
+  private findHistoryPrimaryVideoUrl(record: HistoryRecord, remix?: any): string {
+
+    const result = this.findGeneratedResultForHistory(record);
+
+    return record.resultUrl
+
+      || remix?.stitchUrl
+
+      || result?.url
+
+      || remix?.segments?.find((segment: any) => segment?.videoUrl)?.videoUrl
+
+      || result?.segments?.find((segment: any) => segment?.videoUrl)?.videoUrl
+
+      || '';
+
+  }
+
+
+
+  private buildHistoryDetailSegments(record: HistoryRecord, remix?: any): { id: string; videoUrl: string; prompt: string; status: string; narration?: string }[] {
+
+    const remixSegments = Array.isArray(remix?.segments) ? remix.segments : [];
+
+    const resultSegments = Array.isArray(this.findGeneratedResultForHistory(record)?.segments)
+
+      ? this.findGeneratedResultForHistory(record)?.segments || []
+
+      : [];
+
+    const sourceSegments = remixSegments.length > 0 ? remixSegments : resultSegments;
+
+
+
+    return sourceSegments.map((segment: any, index: number) => ({
+
+      id: segment?.id || `SEG-${index + 1}`,
+
+      videoUrl: segment?.videoUrl || '',
+
+      prompt: segment?.prompt || '',
+
+      status: segment?.status || 'completed',
+
+      narration: segment?.narration || segment?.prompt || ''
+
+    }));
+
+  }
+
+
+
+  // 重新生成任务
+
+  regenerateTask(record: HistoryRecord): void {
+
+    console.log('重新生成任务:', record);
+
+    this.searchKeyword = record.keyword;
+
+    this.setCurrentTab('search');
+
+  }
+
+
+
+  // 导出历史记录
+
+  exportHistory(): void {
+
+    const dataStr = JSON.stringify(this.historyRecords, null, 2);
+
+    const dataBlob = new Blob([dataStr], { type: 'application/json' });
+
+
+
+    const link = document.createElement('a');
+
+    link.href = URL.createObjectURL(dataBlob);
+
+    link.download = `history-${Date.now()}.json`;
+
+    link.click();
+
+  }
+
+
+
+  // 预览结果
+
+  previewResult(result: GeneratedResult): void {
+
+    if (result.type === 'video') {
+
+      this.previewVideoUrl = result.url;
+
+      this.previewDownloadUrl = result.url;
+
+      this.previewVideoTitle = result.title;
+
+      this.previewSourceVideo = null;
+
+      this.previewVideoInfo = {
+
+        quality: result.quality,
+
+        duration: result.duration,
+
+        cost: result.cost,
+
+        created_at: result.created_at
+
+      };
+
+      this.showPreviewModal = true;
+
+    } else {
+
+      window.open(result.url, '_blank');
+
+    }
+
+  }
+
+
+
+  // 下载结果
+
+  downloadResult(result: GeneratedResult): void {
+
+    console.log('下载结果:', result);
+
+    const link = document.createElement('a');
+
+    link.href = result.url;
+
+    link.download = `${result.title}.${result.type === 'video' ? 'mp4' : 'jpg'}`;
+
+    link.click();
+
+  }
+
+
+
+  // 分享结果
+
+  shareResult(result: GeneratedResult): void {
+
+    if (navigator.share) {
+
+      navigator.share({
+
+        title: result.title,
+
+        text: '查看这个由AI生成的精彩内容!',
+
+        url: result.url
+
+      });
+
+    } else {
+
+      navigator.clipboard.writeText(result.url).then(() => {
+
+        alert('链接已复制到剪贴板');
+
+      });
+
+    }
+
+  }
+
+
+
+  // 视频管理相关方法
+
+
+
+  // 获取过滤后的视频列表
+
+  getFilteredVideos(): ManagedVideo[] {
+
+    return this.managedVideos.filter(video => {
+
+      const matchesCategory = !this.videoFilter.category || video.category === this.videoFilter.category;
+
+      const matchesSource = !this.videoFilter.source || video.source === this.videoFilter.source;
+
+      const matchesSearch = !this.videoFilter.searchTerm ||
+
+        video.title.toLowerCase().includes(this.videoFilter.searchTerm.toLowerCase()) ||
+
+        video.description.toLowerCase().includes(this.videoFilter.searchTerm.toLowerCase()) ||
+
+        video.tags.some(tag => tag.toLowerCase().includes(this.videoFilter.searchTerm.toLowerCase()));
+
+
+
+      return matchesCategory && matchesSource && matchesSearch;
+
+    });
+
+  }
+
+
+
+  // 切换视频选择
+
+  toggleVideoSelection(videoId: string): void {
+
+    if (this.selectedVideos.has(videoId)) {
+
+      this.selectedVideos.delete(videoId);
+
+    } else {
+
+      this.selectedVideos.add(videoId);
+
+    }
+
+  }
+
+
+
+  // 获取视频可播放 URL(用于封面截帧等)
+
+  getVideoPlayUrl(video: ManagedVideo): string {
+
+    if (!video) return '';
+
+    // 远程 URL(生成的视频)
+
+    if (video.filepath && /^https?:\/\//.test(video.filepath)) return video.filepath;
+
+    // 本地文件
+
+    if (video.filepath) return video.filepath;
+
+    if (video.filename) return `/backend/api/video/${video.filename}`;
+
+    return '';
+
+  }
+
+
+
+  // 获取来源文本
+
+  getSourceText(source: string): string {
+
+    switch (source) {
+
+      case 'downloaded': return '下载';
+
+      case 'generated': return 'AI生成';
+
+      case 'uploaded': return '上传';
+
+      default: return source;
+
+    }
+
+  }
+
+
+
+  // 获取分类名称
+
+  getCategoryName(categoryId: string): string {
+
+    const category = this.videoCategories.find(cat => cat.id === categoryId);
+
+    return category ? category.name : '未分类';
+
+  }
+
+
+
+  // 获取分类颜色
+
+  getCategoryColor(categoryId: string): string {
+
+    const category = this.videoCategories.find(cat => cat.id === categoryId);
+
+    return category ? category.color : '#757575';
+
+  }
+
+
+
+  // 预览视频
+
+  previewVideo(video: ManagedVideo): void {
+
+    this.previewVideoUrl = video.filepath;
+
+    this.previewDownloadUrl = video.filepath;
+
+    this.previewVideoTitle = video.title;
+
+    this.previewSourceVideo = null;
+
+    this.previewVideoInfo = {
+
+      filename: video.filename,
+
+      size: video.size,
+
+      duration: video.duration,
+
+      resolution: video.metadata?.resolution || '',
+
+      format: video.metadata?.format || 'mp4',
+
+      source: video.source,
+
+      description: video.description,
+
+      tags: video.tags,
+
+      created_at: video.created_at
+
+    };
+
+    this.showPreviewModal = true;
+
+  }
+
+
+
+  // 关闭预览
+
+  closePreview(): void {
+
+    this.showPreviewModal = false;
+
+    this.previewVideoUrl = '';
+
+    this.previewDownloadUrl = '';
+
+    this.previewVideoTitle = '';
+
+    this.previewVideoInfo = null;
+
+    this.previewSourceVideo = null;
+
+  }
+
+
+
+  // 编辑视频
+
+  editVideo(video: ManagedVideo): void {
+
+    console.log('编辑视频:', video);
+
+    // 这里可以打开视频编辑模态框
+
+    const newTitle = prompt('编辑视频标题:', video.title);
+
+    if (newTitle && newTitle !== video.title) {
+
+      video.title = newTitle;
+
+      video.modified_at = new Date();
+
+      console.log('视频标题已更新');
+
+    }
+
+  }
+
+
+
+  // 删除单个视频
+
+  deleteVideo(video: ManagedVideo): void {
+
+    if (confirm(`确定要删除视频「${video.title}」吗?`)) {
+
+      this.http.delete<any>(`/backend/api/manifest/${video.id}`).subscribe({
+
+        next: () => {
+
+          this.managedVideos = this.managedVideos.filter(v => v.id !== video.id);
+
+          this.selectedVideos.delete(video.id);
+
+          this.updateCategoryCounts();
+
+          this.showToast(`🗑️ 视频「${video.title}」已删除`, 'success');
+
+          this.refreshView();
+
+          console.log('视频已删除:', video.title);
+
+        },
+
+        error: (err) => {
+
+          const message = err?.error?.error || '删除视频失败';
+
+          this.showToast(`❌ ${message}`, 'error');
+
+          console.warn('⚠️ 删除视频失败:', err);
+
+          this.refreshView();
+
+        }
+
+      });
+
+    }
+
+  }
+
+
+
+  // 批量删除视频
+
+  batchDelete(): void {
+
+    if (this.selectedVideos.size === 0) return;
+
+
+
+    if (confirm(`确定要删除选中的 ${this.selectedVideos.size} 个视频吗?`)) {
+
+      const selectedIds = Array.from(this.selectedVideos);
+
+      const selectedVideos = this.managedVideos.filter(v => this.selectedVideos.has(v.id));
+
+      let completed = 0;
+
+      let successCount = 0;
+
+
+
+      selectedVideos.forEach((video) => {
+
+        this.http.delete<any>(`/backend/api/manifest/${video.id}`).subscribe({
+
+          next: () => {
+
+            successCount += 1;
+
+            this.managedVideos = this.managedVideos.filter(v => v.id !== video.id);
+
+            this.selectedVideos.delete(video.id);
+
+            completed += 1;
+
+
+
+            if (completed === selectedIds.length) {
+
+              this.isSelectionMode = false;
+
+              this.selectedVideos.clear();
+
+              this.updateCategoryCounts();
+
+              this.showToast(`🗑️ 已删除 ${successCount} 个视频`, 'success');
+
+              this.refreshView();
+
+              console.log('批量视频已删除');
+
+            }
+
+          },
+
+          error: (err) => {
+
+            completed += 1;
+
+            console.warn(`⚠️ 删除视频失败: ${video.title}`, err);
+
+
+
+            if (completed === selectedIds.length) {
+
+              this.isSelectionMode = false;
+
+              this.selectedVideos.clear();
+
+              this.updateCategoryCounts();
+
+              if (successCount > 0) {
+
+                this.showToast(`⚠️ 部分删除成功,已删除 ${successCount}/${selectedIds.length} 个视频`, 'warn');
+
+              } else {
+
+                const message = err?.error?.error || '批量删除失败';
+
+                this.showToast(`❌ ${message}`, 'error');
+
+              }
+
+              this.refreshView();
+
+            }
+
+          }
+
+        });
+
+      });
+
+    }
+
+  }
+
+
+
+  // 批量导出视频
+
+  batchExport(): void {
+
+    if (this.selectedVideos.size === 0) return;
+
+
+
+    const selectedVideos = this.managedVideos.filter(v => this.selectedVideos.has(v.id));
+
+    console.log('批量导出视频:', selectedVideos.map(v => v.title));
+
+    alert(`正在导出 ${selectedVideos.length} 个视频...`);
+
+  }
+
+
+
+  // 批量分类
+
+  batchCategorize(): void {
+
+    if (this.selectedVideos.size === 0) return;
+
+
+
+    const categoryId = prompt(`选择分类:\n${this.videoCategories.map((cat, index) =>
+
+      `${index + 1}. ${cat.name}`).join('\n')}`);
+
+
+
+    if (categoryId) {
+
+      const categoryIndex = parseInt(categoryId) - 1;
+
+      if (categoryIndex >= 0 && categoryIndex < this.videoCategories.length) {
+
+        const selectedCategory = this.videoCategories[categoryIndex];
+
+
+
+        this.managedVideos.forEach(video => {
+
+          if (this.selectedVideos.has(video.id)) {
+
+            video.category = selectedCategory.id;
+
+            video.modified_at = new Date();
+
+          }
+
+        });
+
+
+
+        this.updateCategoryCounts();
+
+        this.selectedVideos.clear();
+
+        this.isSelectionMode = false;
+
+        console.log('批量分类完成');
+
+      }
+
+    }
+
+  }
+
+
+
+  // 更新分类计数
+
+  private updateCategoryCounts(): void {
+
+    this.videoCategories.forEach(category => {
+
+      category.count = this.managedVideos.filter(video => video.source === category.id || video.category === category.id).length;
+
+    });
+
+  }
+
+
+
+  // ==================== 视频上传 ====================
+
+
+
+  openUploadModal(): void {
+
+    this.showUploadModal = true;
+
+    this.uploadTitle = '';
+
+    this.uploadDescription = '';
+
+    this.uploadTags = '';
+
+    this.uploadFile = null;
+
+    this.uploadFileName = '';
+
+    this.uploadProgress = 0;
+
+    this.uploadError = '';
+
+    this.isUploading = false;
+
+  }
+
+
+
+  closeUploadModal(): void {
+
+    if (this.isUploading) return;
+
+    this.showUploadModal = false;
+
+    this.uploadFile = null;
+
+    this.uploadFileName = '';
+
+  }
+
+
+
+  onFileSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (input.files && input.files.length > 0) {
+
+      const file = input.files[0];
+
+      const allowedExts = ['.mp4', '.mov', '.avi', '.webm', '.mkv'];
+
+      const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+
+
+
+      if (!allowedExts.includes(ext)) {
+
+        this.uploadError = '不支持的文件格式,仅支持: mp4, mov, avi, webm, mkv';
+
+        input.value = '';
+
+        return;
+
+      }
+
+
+
+      if (file.size > 500 * 1024 * 1024) {
+
+        this.uploadError = '文件大小超过限制(最大 500MB)';
+
+        input.value = '';
+
+        return;
+
+      }
+
+
+
+      this.uploadFile = file;
+
+      this.uploadFileName = file.name;
+
+      this.uploadError = '';
+
+
+
+      // 自动填充标题(去掉扩展名)
+
+      if (!this.uploadTitle) {
+
+        this.uploadTitle = file.name.replace(/\.[^.]+$/, '');
+
+      }
+
+    }
+
+  }
+
+
+
+  uploadVideo(): void {
+
+    if (!this.uploadFile || this.isUploading) return;
+
+
+
+    this.isUploading = true;
+
+    this.uploadProgress = 0;
+
+    this.uploadError = '';
+
+
+
+    const formData = new FormData();
+
+    formData.append('video', this.uploadFile);
+
+    formData.append('title', this.uploadTitle || this.uploadFile.name.replace(/\.[^.]+$/, ''));
+
+    formData.append('description', this.uploadDescription || '用户上传的视频');
+
+    if (this.uploadTags.trim()) {
+
+      const tagsArr = this.uploadTags.split(/[,,、\s]+/).filter(t => t.trim());
+
+      formData.append('tags', JSON.stringify(tagsArr));
+
+    }
+
+
+
+    const xhr = new XMLHttpRequest();
+
+    xhr.open('POST', '/backend/api/upload/video', true);
+
+
+
+    xhr.upload.onprogress = (e) => {
+
+      if (e.lengthComputable) {
+
+        this.ngZone.run(() => {
+
+          this.uploadProgress = Math.round((e.loaded / e.total) * 100);
+
+          this.refreshView();
+
+        });
+
+      }
+
+    };
+
+
+
+    xhr.onload = () => {
+
+      this.ngZone.run(() => {
+
+        this.isUploading = false;
+
+        if (xhr.status === 200) {
+
+          try {
+
+            const res = JSON.parse(xhr.responseText);
+
+            if (res.success && res.video) {
+
+              this.uploadProgress = 100;
+
+              const newVideo: ManagedVideo = {
+
+                id: res.video.id,
+
+                title: res.video.title,
+
+                filename: res.video.filename,
+
+                filepath: res.filepath || `/backend/api/video/${res.video.filename}`,
+
+                size: res.video.size,
+
+                duration: res.video.duration || 0,
+
+                created_at: new Date(),
+
+                modified_at: new Date(),
+
+                category: 'uploaded',
+
+                tags: res.video.tags || [],
+
+                description: res.video.description || '',
+
+                thumbnail: '',
+
+                source: 'uploaded',
+
+                metadata: res.video.metadata || { resolution: '未知', format: 'mp4' }
+
+              };
+
+              this.managedVideos.unshift(newVideo);
+
+              this.updateCategoryCounts();
+
+
+
+              this.showUploadModal = false;
+
+              this.uploadFile = null;
+
+              this.uploadFileName = '';
+
+              this.showToast(`✅ 视频「${newVideo.title}」上传成功!`, 'success');
+
+              console.log('📤 视频上传成功:', newVideo);
+
+            } else {
+
+              this.uploadError = res.error || '上传失败';
+
+            }
+
+          } catch {
+
+            this.uploadError = '解析响应失败';
+
+          }
+
+        } else {
+
+          try {
+
+            const errRes = JSON.parse(xhr.responseText);
+
+            this.uploadError = errRes.error || `上传失败 (${xhr.status})`;
+
+          } catch {
+
+            this.uploadError = `上传失败 (HTTP ${xhr.status})`;
+
+          }
+
+        }
+
+        this.refreshView();
+
+      });
+
+    };
+
+
+
+    xhr.onerror = () => {
+
+      this.ngZone.run(() => {
+
+        this.isUploading = false;
+
+        this.uploadError = '网络错误,请检查后端服务是否运行';
+
+        this.refreshView();
+
+      });
+
+    };
+
+
+
+    xhr.send(formData);
+
+  }
+
+
+
+  // ==================== AI重塑 — 多步骤流程 ====================
+
+
+
+  // Step 1: 打开AI重塑模态框
+
+  aiRemixVideo(video: ManagedVideo): void {
+
+    if (video.isRemixing) return;
+
+    this.remixTargetVideo = video;
+
+    this.remixStep = 1;
+
+    this.remixTranscript = '';
+
+    this.remixBeautifiedTranscript = '';
+
+    this.remixStoryboard = [];
+
+    this.remixStyle = 'anime';
+
+    this.remixQuality = '720p';
+
+    this.remixFrames = 121;
+
+    this.remixAspectRatio = '16:9';
+
+    this.remixStatusText = '';
+
+    this.isRemixSubmitting = false;
+
+    this.remixResults = { total: 0, success: 0, failed: 0 };
+
+    this.remixStitchStatus = 'idle';
+
+    this.remixStitchUrl = '';
+
+    this.remixStitchError = '';
+
+    this.remixFinalVideoUrl = '';
+
+    this.remixMode = 'standard';
+
+    this.resetDigitalHumanState();
+
+    this.isWhisperRunning = false;
+
+    this.whisperStatusText = '';
+
+    this.showManualTranscriptInput = false;
+
+    this.showRemixModal = true;
+
+
+
+    // 自动尝试加载Whisper处理后的文件
+
+    this.loadWhisperFiles(video);
+
+  }
+
+
+
+  // 关闭重塑模态框
+
+  closeRemixModal(): void {
+
+    if (this.isRemixSubmitting) return;
+
+    this.showRemixModal = false;
+
+    this.remixTargetVideo = null;
+
+    this.resetDigitalHumanState();
+
+  }
+
+
+
+  // ========== AI重塑模式选择弹窗 ==========
+
+  openRemixModeChooser(video: ManagedVideo): void {
+
+    if (!video || video.isRemixing) return;
+
+    this.remixModeChooserVideo = video;
+
+    this.showRemixModeChooser = true;
+
+  }
+
+
+
+  closeRemixModeChooser(): void {
+
+    this.showRemixModeChooser = false;
+
+    this.remixModeChooserVideo = null;
+
+  }
+
+
+
+  selectRemixMode(mode: 'standard' | 'digital-human'): void {
+
+    const video = this.remixModeChooserVideo;
+
+    this.showRemixModeChooser = false;
+
+    this.remixModeChooserVideo = null;
+
+    if (!video) return;
+
+
+
+    if (mode === 'digital-human') {
+
+      // 数字人生成:跳转到数字人合成页,并把该视频作为「参考视频」自动加载 + 分析
+
+      this.setCurrentTab('digital-human');
+
+      this.startDhRefFromManagedVideo(video);
+
+      return;
+
+    }
+
+
+
+    // 标准AI重塑:跳转到视频生成页,并将该视频作为已上传文件自动完成第一步
+
+    this.setCurrentTab('video-generation');
+
+    this.startVgFromManagedVideo(video);
+
+  }
+
+
+
+  private async startVgFromManagedVideo(video: ManagedVideo): Promise<void> {
+
+    try {
+
+      this.vgReset();
+
+
+
+      const playUrl = this.getVideoPlayUrl(video);
+
+      const displayName = video.title || video.filename || `video-${video.id}`;
+
+
+
+      // 优先把已存在的视频抓取为 File 对象,便于 ≤20MB 走 Gemini 直接识别
+
+      let file: File | null = null;
+
+      if (playUrl) {
+
+        try {
+
+          const resp = await fetch(playUrl);
+
+          if (resp.ok) {
+
+            const blob = await resp.blob();
+
+            const mime = blob.type || 'video/mp4';
+
+            const fileName = video.filename || `${displayName}.mp4`;
+
+            file = new File([blob], fileName, { type: mime });
+
+          }
+
+        } catch (err) {
+
+          console.warn('从已存在视频构造 File 失败,将走音频提取链路:', err);
+
+        }
+
+      }
+
+
+
+      this.vgSourceFile = file;
+
+      this.vgSourceFileName = displayName;
+
+      this.vgSourceVideoUrl = playUrl;
+
+      this.vgVideoId = video.id;
+
+      this.vgStep = 2;
+
+      this.vgUploading = false;
+
+      this.vgUploadProgress = 100;
+
+      this.vgError = '';
+
+
+
+      // 复用上传成功后的后续流程:建任务 + 启动解析
+
+      this.vgCreateTask();
+
+      this.vgUpdateTaskProgress('vg-analyze', 15);
+
+      this.vgStartAnalysis();
+
+      this.cdr.detectChanges();
+
+    } catch (err: any) {
+
+      console.error('启动标准AI重塑失败:', err);
+
+      this.showToast(`❌ 启动标准AI重塑失败:${err?.message || '请重试'}`, 'error');
+
+    }
+
+  }
+
+
+
+  // ========================================================================
+
+  //  数字人合成页:参考视频(独立的上传 + 分析流程,与 vg* 完全隔离)
+
+  // ========================================================================
+
+
+
+  // 选择本地参考视频文件
+
+  dhRefSelectFile(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    const file = input?.files?.[0];
+
+    if (!file) return;
+
+    if (!file.type.startsWith('video/')) {
+
+      this.showToast('请选择视频文件', 'warn');
+
+      return;
+
+    }
+
+    this.dhRefRevokeUrl();
+
+    this.dhRefFile = file;
+
+    this.dhRefFileName = file.name;
+
+    this.dhRefFileSize = file.size;
+
+    this.dhRefVideoUrl = URL.createObjectURL(file);
+
+    this.dhRefVideoId = '';
+
+    this.dhRefError = '';
+
+    this.dhRefTranscript = '';
+
+    this.dhRefAnalysisText = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  // 清除参考视频与重塑状态
+
+  dhRefClear(): void {
+
+    this.dhRefRevokeUrl();
+
+    this.dhRefFile = null;
+
+    this.dhRefFileName = '';
+
+    this.dhRefFileSize = 0;
+
+    this.dhRefVideoUrl = '';
+
+    this.dhRefVideoId = '';
+
+    this.dhRefUploading = false;
+
+    this.dhRefUploadProgress = 0;
+
+    this.dhRefAnalyzing = false;
+
+    this.dhRefAnalysisText = '';
+
+    this.dhRefTranscript = '';
+
+    this.dhRefError = '';
+
+    this.dhRefRewriting = false;
+
+    this.dhRefRewriteError = '';
+
+    this.dhRefAutoRewrite = false;
+
+    this.refreshView();
+
+  }
+
+
+
+  private dhRefRevokeUrl(): void {
+
+    if (this.dhRefVideoUrl && this.dhRefVideoUrl.startsWith('blob:')) {
+
+      try { URL.revokeObjectURL(this.dhRefVideoUrl); } catch {}
+
+    }
+
+  }
+
+
+
+  // 从视频管理页选中视频后进入:直接复用已存在的视频文件,跳过上传,自动开始分析 + 重塑
+
+  private async startDhRefFromManagedVideo(video: ManagedVideo): Promise<void> {
+
+    try {
+
+      this.dhRefClear();
+
+      this.dhRefAutoRewrite = true;
+
+
+
+      const playUrl = this.getVideoPlayUrl(video);
+
+      const displayName = video.title || video.filename || `video-${video.id}`;
+
+
+
+      // 优先把已存在视频抓为 File(≤20MB 可走 Gemini 直传)
+
+      let file: File | null = null;
+
+      if (playUrl) {
+
+        try {
+
+          const resp = await fetch(playUrl);
+
+          if (resp.ok) {
+
+            const blob = await resp.blob();
+
+            const mime = blob.type || 'video/mp4';
+
+            const fileName = video.filename || `${displayName}.mp4`;
+
+            file = new File([blob], fileName, { type: mime });
+
+          }
+
+        } catch (err) {
+
+          console.warn('⚠️ 数字人参考视频:从已存在视频构造 File 失败:', err);
+
+        }
+
+      }
+
+
+
+      this.dhRefFile = file;
+
+      this.dhRefFileName = displayName;
+
+      this.dhRefFileSize = file ? file.size : 0;
+
+      this.dhRefVideoUrl = playUrl || '';
+
+      this.dhRefVideoId = video.id;
+
+      this.dhRefError = '';
+
+      this.refreshView();
+
+
+
+      this.dhRefStartAnalysis();
+
+    } catch (err: any) {
+
+      console.error('启动数字人参考视频失败:', err);
+
+      this.showToast(`❌ 启动数字人参考视频失败:${err?.message || '请重试'}`, 'error');
+
+    }
+
+  }
+
+
+
+  // 上传本地参考视频,再触发分析
+
+  dhRefUploadAndAnalyze(): void {
+
+    if (!this.dhRefFile || this.dhRefUploading) return;
+
+    this.dhRefUploading = true;
+
+    this.dhRefUploadProgress = 0;
+
+    this.dhRefError = '';
+
+    this.refreshView();
+
+
+
+    const formData = new FormData();
+
+    formData.append('video', this.dhRefFile);
+
+    formData.append('title', `数字人参考视频 - ${new Date().toLocaleString('zh-CN')}`);
+
+
+
+    const xhr = new XMLHttpRequest();
+
+    xhr.open('POST', '/backend/api/upload/video');
+
+    xhr.upload.onprogress = (e) => {
+
+      if (e.lengthComputable) {
+
+        this.dhRefUploadProgress = Math.round((e.loaded / e.total) * 100);
+
+        this.cdr.detectChanges();
+
+      }
+
+    };
+
+    xhr.onload = () => {
+
+      this.ngZone.run(() => {
+
+        this.dhRefUploading = false;
+
+        if (xhr.status === 200) {
+
+          try {
+
+            const res = JSON.parse(xhr.responseText);
+
+            if (res?.success && res?.video?.id) {
+
+              this.dhRefVideoId = res.video.id;
+
+              this.dhRefStartAnalysis();
+
+            } else {
+
+              this.dhRefError = '上传返回异常';
+
+            }
+
+          } catch { this.dhRefError = '解析上传结果失败'; }
+
+        } else {
+
+          this.dhRefError = `上传失败 (${xhr.status})`;
+
+        }
+
+        this.cdr.detectChanges();
+
+      });
+
+    };
+
+    xhr.onerror = () => {
+
+      this.ngZone.run(() => {
+
+        this.dhRefUploading = false;
+
+        this.dhRefError = '网络错误,上传失败';
+
+        this.cdr.detectChanges();
+
+      });
+
+    };
+
+    xhr.send(formData);
+
+  }
+
+
+
+  // 启动分析:≤20MB 直传 Gemini,>20MB 提取音轨给 Gemini 音频识别,最后降级 Whisper
+
+  private dhRefStartAnalysis(): void {
+
+    this.dhRefAnalyzing = true;
+
+    this.dhRefTranscript = '';
+
+    this.dhRefAnalysisText = '';
+
+    this.refreshView();
+
+
+
+    const sizeMB = this.dhRefFile ? this.dhRefFile.size / 1024 / 1024 : 0;
+
+    if (this.dhRefFile && sizeMB <= 20) {
+
+      this.dhRefAnalysisText = `正在将视频 (${sizeMB.toFixed(1)}MB) 发送给 Gemini 进行视频理解...`;
+
+      this.refreshView();
+
+      this.dhRefRunGeminiVideo();
+
+    } else if (this.dhRefVideoId) {
+
+      this.dhRefAnalysisText = `视频 ${sizeMB.toFixed(1)}MB 超过 Gemini 视频限制,正在提取音轨...`;
+
+      this.refreshView();
+
+      this.dhRefRunAudioGemini();
+
+    } else {
+
+      this.dhRefAnalysisText = '使用 Whisper 语音识别...';
+
+      this.refreshView();
+
+      this.dhRefRunWhisperFallback();
+
+    }
+
+  }
+
+
+
+  private async dhRefRunGeminiVideo(): Promise<void> {
+
+    try {
+
+      const base64 = await this.llmService.fileToBase64(this.dhRefFile!);
+
+      const mimeType = this.dhRefFile!.type || 'video/mp4';
+
+      const prompt = this.buildAnalysisPrompt('video');
+
+      this.llmService.analyzeVideo(base64, mimeType, prompt, {
+
+        model: 'gemini-2.5-flash',
+
+        generationConfig: { maxOutputTokens: 8192 }
+
+      }).subscribe({
+
+        next: (result) => {
+
+          this.dhRefParseGeminiResult(result);
+
+          this.dhRefAnalyzing = false;
+
+          this.refreshView();
+
+          this.dhRefMaybeAutoRewrite();
+
+        },
+
+        error: (err) => {
+
+          console.warn('⚠️ Gemini 视频识别失败,降级 Whisper:', err);
+
+          this.dhRefAnalysisText = 'Gemini 识别失败,正在使用 Whisper 语音识别...';
+
+          this.refreshView();
+
+          this.dhRefRunWhisperFallback();
+
+        }
+
+      });
+
+    } catch (err) {
+
+      console.warn('⚠️ 视频文件读取失败:', err);
+
+      this.dhRefAnalysisText = '视频文件读取失败,正在使用 Whisper 语音识别...';
+
+      this.refreshView();
+
+      this.dhRefRunWhisperFallback();
+
+    }
+
+  }
+
+
+
+  private dhRefRunAudioGemini(): void {
+
+    this.http.post<any>('/backend/api/extract-audio-mp3', { videoId: this.dhRefVideoId }).subscribe({
+
+      next: (res) => {
+
+        if (res?.success && res?.audio?.base64) {
+
+          const audioSizeMB = res.audio.sizeMB || 0;
+
+          this.dhRefAnalysisText = `音轨提取完成 (${audioSizeMB.toFixed(1)}MB),正在 Gemini 音频识别...`;
+
+          this.refreshView();
+
+          if (audioSizeMB > 20) {
+
+            this.dhRefAnalysisText = '音频文件仍较大,改用 Whisper 识别...';
+
+            this.refreshView();
+
+            this.dhRefRunWhisperFallback();
+
+            return;
+
+          }
+
+          const prompt = this.buildAnalysisPrompt('audio');
+
+          this.llmService.analyzeAudio(res.audio.base64, res.audio.mimeType || 'audio/mp4', prompt, {
+
+            model: 'gemini-2.5-flash',
+
+            generationConfig: { maxOutputTokens: 8192 }
+
+          }).subscribe({
+
+            next: (result) => {
+
+              this.dhRefParseGeminiResult(result);
+
+              this.dhRefAnalyzing = false;
+
+              this.refreshView();
+
+              this.dhRefMaybeAutoRewrite();
+
+            },
+
+            error: (err) => {
+
+              console.warn('⚠️ Gemini 音频识别失败,降级 Whisper:', err);
+
+              this.dhRefAnalysisText = 'Gemini 音频识别失败,改用 Whisper...';
+
+              this.refreshView();
+
+              this.dhRefRunWhisperFallback();
+
+            }
+
+          });
+
+        } else {
+
+          this.dhRefRunWhisperFallback();
+
+        }
+
+      },
+
+      error: (err) => {
+
+        const detail = err?.error?.error || err?.message || `HTTP ${err?.status}`;
+
+        console.warn('⚠️ 音轨提取失败:', detail, err);
+
+        this.dhRefAnalysisText = `音轨提取失败 (${detail}),改用 Whisper...`;
+
+        this.refreshView();
+
+        this.dhRefRunWhisperFallback();
+
+      }
+
+    });
+
+  }
+
+
+
+  private dhRefRunWhisperFallback(): void {
+
+    if (!this.dhRefVideoId) {
+
+      this.dhRefAnalyzing = false;
+
+      this.dhRefAnalysisText = '无法降级到 Whisper:参考视频尚未上传到服务端';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    this.http.post<any>('/backend/api/whisper/transcribe', {
+
+      videoId: this.dhRefVideoId,
+
+      language: 'Chinese',
+
+      model: 'large'
+
+    }).subscribe({
+
+      next: (res) => {
+
+        if (res?.success && res?.transcript) {
+
+          this.dhRefTranscript = res.transcript;
+
+          this.dhRefAnalysisText = 'Whisper 转录完成,正在 AI 分析内容...';
+
+          this.dhRefAutoFillTts();
+
+          this.refreshView();
+
+          this.dhRefDoTextAnalysis();
+
+        } else {
+
+          this.dhRefAnalyzing = false;
+
+          this.dhRefAnalysisText = '语音识别完成但未返回文字稿,请手动输入';
+
+          this.refreshView();
+
+        }
+
+      },
+
+      error: (err) => {
+
+        this.dhRefAnalyzing = false;
+
+        this.dhRefAnalysisText = `Whisper 识别失败: ${err?.error?.error || err?.message || '未知错误'}。请手动输入文字稿。`;
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  private dhRefDoTextAnalysis(): void {
+
+    const systemPrompt = `你是一个专业的视频内容分析师。请基于以下视频转录文字稿,提取:
+
+1. 视频主题和核心观点
+
+2. 目标受众
+
+3. 内容风格
+
+4. 关键知识点(3-5条)
+
+5. 改进方向
+
+请用简洁中文回复。`;
+
+    this.llmService.askWithSystem(systemPrompt, this.dhRefTranscript, { model: 'gemini-2.5-flash', max_tokens: 1024 }).subscribe({
+
+      next: (analysis) => {
+
+        this.dhRefAnalysisText = analysis || '分析完成';
+
+        this.dhRefAnalyzing = false;
+
+        this.refreshView();
+
+        this.dhRefMaybeAutoRewrite();
+
+      },
+
+      error: () => {
+
+        this.dhRefAnalysisText = '(AI 文本分析暂不可用,已完成 Whisper 转录)';
+
+        this.dhRefAnalyzing = false;
+
+        this.refreshView();
+
+        this.dhRefMaybeAutoRewrite();
+
+      }
+
+    });
+
+  }
+
+
+
+  private dhRefParseGeminiResult(raw: string): void {
+
+    const m = raw.match(/---TRANSCRIPT_START---([\s\S]*?)---TRANSCRIPT_END---/);
+
+    if (m) {
+
+      this.dhRefTranscript = m[1].trim();
+
+      const analysisPart = raw.substring(0, raw.indexOf('---TRANSCRIPT_START---')).trim();
+
+      this.dhRefAnalysisText = analysisPart.replace(/^【内容分析】\s*/, '') || '分析完成';
+
+    } else {
+
+      this.dhRefAnalysisText = raw || '分析完成';
+
+    }
+
+    this.dhRefAutoFillTts();
+
+  }
+
+
+
+  /** 把参考视频识别得到的文字稿自动填入 Step 2 口播文本(不覆盖用户已手输的内容) */
+
+  private dhRefAutoFillTts(): void {
+
+    const transcript = (this.dhRefTranscript || '').trim();
+
+    if (!transcript) return;
+
+    // 仅在口播文本为空时自动填入;用户已输入则保留
+
+    if (!this.dhTtsText || !this.dhTtsText.trim()) {
+
+      this.dhTtsText = transcript.slice(0, 600);
+
+    }
+
+  }
+
+
+
+  /** 文字稿手动编辑时,同步反映到口播文本(前提:当前口播仍等于上一次同步值) */
+
+  dhRefSyncToTts(newTranscript: string): void {
+
+    const next = (newTranscript || '').slice(0, 600);
+
+    this.dhTtsText = next;
+
+  }
+
+
+
+  /** 应用补充提示词预设;二次点击同一个则取消 */
+
+  dhApplyPromptPreset(value: string): void {
+
+    if (this.dhPrompt === value) {
+
+      this.dhPrompt = '';
+
+    } else {
+
+      this.dhPrompt = value;
+
+    }
+
+    this.refreshView();
+
+  }
+
+
+
+  private buildAnalysisPrompt(kind: 'video' | 'audio'): string {
+
+    const taskOne = kind === 'video'
+
+      ? `从画面和音频两个维度分析:1.主题/核心观点 2.画面描述 3.目标受众 4.内容风格 5.亮点(3-5条) 6.视觉风格 7.改进建议`
+
+      : `从音频分析:1.主题/核心观点 2.目标受众 3.内容风格 4.亮点(3-5条) 5.改进建议`;
+
+    return `你是一个专业的内容分析师。请完成两个任务:
+
+
+
+===== 任务一:内容分析 =====
+
+${taskOne}
+
+
+
+===== 任务二:完整语音转录 =====
+
+请将所有语音内容逐字转录为中文,忠实还原、不遗漏、不改写、不总结,按自然段落分段。
+
+
+
+请严格按以下格式输出(用分隔线区分两部分):
+
+
+
+【内容分析】
+
+(任务一结果)
+
+
+
+---TRANSCRIPT_START---
+
+(任务二完整转录)
+
+---TRANSCRIPT_END---`;
+
+  }
+
+
+
+  // 视频分析完成后自动触发(仅在通过 AI重塑入口或显式标记时)
+
+  private dhRefMaybeAutoRewrite(): void {
+
+    if (!this.dhRefAutoRewrite) return;
+
+    if (!this.dhRefTranscript || !this.dhRefTranscript.trim()) return;
+
+    this.dhRefAutoRewrite = false; // 仅触发一次
+
+    this.dhRefRunRewrite();
+
+  }
+
+
+
+  // 把当前文字稿 + 内容分析交给 AI 重写为口播文本与补充提示词
+
+  dhRefRunRewrite(): void {
+
+    if (!this.dhRefTranscript || !this.dhRefTranscript.trim()) {
+
+      this.dhRefRewriteError = '暂无可重塑的视频文字稿';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    this.dhRefRewriting = true;
+
+    this.dhRefRewriteError = '';
+
+    this.refreshView();
+
+
+
+    const systemPrompt = `你是一位短视频数字人编剧。基于用户提供的【原始视频文字稿】和【内容分析】,请输出严格的 JSON:
+
+{
+
+  "ttsScript": "重塑后的口播文本,第一人称、自然口语、节奏适合数字人朗读,控制在 200 字以内",
+
+  "dhPrompt": "数字人生成的补充提示词,描述人物动作 / 镜头语言 / 风格要求,控制在 80 字以内"
+
+}
+
+只返回纯 JSON 对象,不要使用 markdown 代码块、不要添加解释或前后缀。注意:JSON 字符串内部禁止出现真实换行,必须使用 \\n 转义。`;
+
+    const userInput = `【原始文字稿】\n${this.dhRefTranscript}\n\n【内容分析】\n${this.dhRefAnalysisText || '(无)'}`;
+
+
+
+    this.llmService.askWithSystem(systemPrompt, userInput, { model: 'gemini-2.5-flash', max_tokens: 2048 }).subscribe({
+
+      next: (text) => {
+
+        this.dhRefRewriting = false;
+
+        const cleaned = (text || '').replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
+
+        const parsed = this.parseRewriteJson(cleaned);
+
+        if (parsed) {
+
+          const tts = typeof parsed.ttsScript === 'string' ? parsed.ttsScript.trim() : '';
+
+          const prompt = typeof parsed.dhPrompt === 'string' ? parsed.dhPrompt.trim() : '';
+
+          if (tts) this.dhTtsText = tts.slice(0, 600);
+
+          if (prompt) this.dhPrompt = prompt.slice(0, 200);
+
+          if (tts || prompt) {
+
+            this.showToast('✅ 已自动填充口播文本与补充提示词', 'success');
+
+          } else {
+
+            this.dhRefRewriteError = 'AI 未返回有效字段,请手动整理';
+
+          }
+
+        } else {
+
+          console.warn('⚠️ 解析重塑结果失败,原始返回:', cleaned);
+
+          this.dhRefRewriteError = '解析重塑结果失败,已将原始文本填入口播文本,请手动整理';
+
+          this.dhTtsText = (text || '').slice(0, 600);
+
+          this.showToast('⚠️ 重塑结果解析失败,已退回原文', 'warn');
+
+        }
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        this.dhRefRewriting = false;
+
+        this.dhRefRewriteError = `AI 重塑失败:${this.getErrorText(err, '请重试')}`;
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  /** 容错解析 LLM 返回的 JSON:支持 fenced 代码块、未转义换行、字段正则兜底 */
+
+  private parseRewriteJson(raw: string): { ttsScript?: string; dhPrompt?: string } | null {
+
+    if (!raw) return null;
+
+    // 1) 严格解析
+
+    try { return JSON.parse(raw); } catch {}
+
+    // 2) 截取首个 { ... } 区段
+
+    const braceStart = raw.indexOf('{');
+
+    const braceEnd = raw.lastIndexOf('}');
+
+    let candidate = braceStart >= 0 && braceEnd > braceStart ? raw.slice(braceStart, braceEnd + 1) : raw;
+
+    // 3) 把字符串内部的裸换行/回车/制表符转义
+
+    const sanitized = this.escapeRawControlCharsInJsonStrings(candidate);
+
+    try { return JSON.parse(sanitized); } catch {}
+
+    // 4) 正则兜底,逐字段抽取(兼容 "tts": "..." 中含 \" 转义)
+
+    const fieldRegex = (key: string) => new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, 'i');
+
+    const ttsMatch = candidate.match(fieldRegex('ttsScript'));
+
+    const promptMatch = candidate.match(fieldRegex('dhPrompt'));
+
+    if (ttsMatch || promptMatch) {
+
+      const unescape = (s: string) => s.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
+
+      return {
+
+        ttsScript: ttsMatch ? unescape(ttsMatch[1]) : undefined,
+
+        dhPrompt: promptMatch ? unescape(promptMatch[1]) : undefined,
+
+      };
+
+    }
+
+    // 5) 截断兜底:若仅有 ttsScript 开头但被截断,抓到截断处为止
+
+    const truncatedTts = candidate.match(/"ttsScript"\s*:\s*"((?:\\.|[^"\\])*)$/i);
+
+    if (truncatedTts) {
+
+      return { ttsScript: truncatedTts[1].replace(/\\n/g, '\n').replace(/\\"/g, '"') };
+
+    }
+
+    return null;
+
+  }
+
+
+
+  /** 在 JSON 字符串字面量内部把裸 \n \r \t 替换为转义形式,括号外的不动 */
+
+  private escapeRawControlCharsInJsonStrings(input: string): string {
+
+    let out = '';
+
+    let inString = false;
+
+    let escape = false;
+
+    for (let i = 0; i < input.length; i++) {
+
+      const ch = input[i];
+
+      if (inString) {
+
+        if (escape) { out += ch; escape = false; continue; }
+
+        if (ch === '\\') { out += ch; escape = true; continue; }
+
+        if (ch === '"') { inString = false; out += ch; continue; }
+
+        if (ch === '\n') { out += '\\n'; continue; }
+
+        if (ch === '\r') { out += '\\r'; continue; }
+
+        if (ch === '\t') { out += '\\t'; continue; }
+
+        out += ch;
+
+      } else {
+
+        if (ch === '"') { inString = true; out += ch; continue; }
+
+        out += ch;
+
+      }
+
+    }
+
+    return out;
+
+  }
+
+
+
+  getActiveRemixSteps(): RemixFlowStep[] {
+
+    if (this.remixMode === 'digital-human') {
+
+      return [
+
+        { step: 1, label: this.digitalHumanStepLabels[0] },
+
+        { step: 4, label: this.digitalHumanStepLabels[1] },
+
+        { step: 5, label: this.digitalHumanStepLabels[2] },
+
+        { step: 6, label: this.digitalHumanStepLabels[3] }
+
+      ];
+
+    }
+
+
+
+    return this.remixStepLabels.map((label, index) => ({ step: index + 1, label }));
+
+  }
+
+
+
+  setRemixMode(mode: 'standard' | 'digital-human'): void {
+
+    this.remixMode = mode;
+
+    this.remixDigitalHumanError = '';
+
+    if (mode === 'standard' && this.remixStep === 4 && this.remixStoryboard.length === 0) {
+
+      this.remixStep = 1;
+
+    }
+
+  }
+
+
+
+  private hydrateVoiceProfile(profile: any): VoiceProfile {
+
+    const status = profile?.status === null || profile?.status === undefined || profile?.status === ''
+
+      ? null
+
+      : Number(profile.status);
+
+    const synthesizedAudioUrl = String(profile?.synthesized_audio_url || '');
+
+    const demoAudio = String(profile?.demo_audio || '');
+
+    const latestAudioUrl = String(profile?.latest_audio_url || synthesizedAudioUrl || demoAudio || '');
+
+
+
+    return {
+
+      ...profile,
+
+      creation_mode: String(profile?.creation_mode || (profile?.timbre_id ? 'clone' : 'design')),
+
+      speaker_id: String(profile?.speaker_id || ''),
+
+      timbre_id: String(profile?.timbre_id || profile?.timbreId || ''),
+
+      sample_text: String(profile?.sample_text || ''),
+
+      text_prompt: String(profile?.text_prompt || ''),
+
+      source_audio_text: String(profile?.source_audio_text || ''),
+
+      source_audio_name: String(profile?.source_audio_name || ''),
+
+      source_audio_format: String(profile?.source_audio_format || ''),
+
+      language: Number(profile?.language ?? 0),
+
+      status,
+
+      status_label: String(profile?.status_label || (demoAudio || synthesizedAudioUrl ? '可用' : '未知')),
+
+      demo_audio: demoAudio,
+
+      available_training_times: profile?.available_training_times === null || profile?.available_training_times === undefined
+
+        ? null
+
+        : Number(profile.available_training_times),
+
+      image_prompt_name: String(profile?.image_prompt_name || ''),
+
+      x_api_resource_id: String(profile?.x_api_resource_id || ''),
+
+      model_version: String(profile?.model_version || ''),
+
+      icl_speaker_id: String(profile?.icl_speaker_id || ''),
+
+      occupied: !!profile?.occupied,
+
+      synthesized_audio_url: synthesizedAudioUrl,
+
+      synthesized_work_id: String(profile?.synthesized_work_id || ''),
+
+      last_synthesis_text: String(profile?.last_synthesis_text || ''),
+
+      latest_audio_url: latestAudioUrl,
+
+      ...this.buildVoiceSynthesisDraft(profile),
+
+      is_synthesizing: false,
+
+      message: String(profile?.message || ''),
+
+      request_id: String(profile?.request_id || ''),
+
+      created_at: new Date(profile?.created_at || Date.now()),
+
+      updated_at: profile?.updated_at ? new Date(profile.updated_at) : undefined
+
+    };
+
+  }
+
+
+
+  private upsertLocalVoiceProfile(profile: VoiceProfile): void {
+
+    const nextProfiles = [...this.voiceProfiles];
+
+    const existingIndex = nextProfiles.findIndex((item) => (
+
+      item.id === profile.id
+
+      || (!!profile.timbre_id && item.timbre_id === profile.timbre_id)
+
+    ));
+
+
+
+    if (existingIndex >= 0) {
+
+      nextProfiles[existingIndex] = {
+
+        ...nextProfiles[existingIndex],
+
+        ...profile,
+
+        draft_synthesis_text: profile.draft_synthesis_text ?? nextProfiles[existingIndex].draft_synthesis_text,
+
+        draft_synthesis_ssml: profile.draft_synthesis_ssml ?? nextProfiles[existingIndex].draft_synthesis_ssml,
+
+        draft_synthesis_x_api_resource_id: profile.draft_synthesis_x_api_resource_id ?? nextProfiles[existingIndex].draft_synthesis_x_api_resource_id,
+
+        draft_synthesis_model: profile.draft_synthesis_model ?? nextProfiles[existingIndex].draft_synthesis_model,
+
+        draft_synthesis_format: profile.draft_synthesis_format ?? nextProfiles[existingIndex].draft_synthesis_format,
+
+        draft_synthesis_sample_rate: profile.draft_synthesis_sample_rate ?? nextProfiles[existingIndex].draft_synthesis_sample_rate,
+
+        draft_synthesis_speech_rate: profile.draft_synthesis_speech_rate ?? nextProfiles[existingIndex].draft_synthesis_speech_rate,
+
+        draft_synthesis_loudness_rate: profile.draft_synthesis_loudness_rate ?? nextProfiles[existingIndex].draft_synthesis_loudness_rate,
+
+        draft_synthesis_emotion: profile.draft_synthesis_emotion ?? nextProfiles[existingIndex].draft_synthesis_emotion,
+
+        draft_synthesis_emotion_scale: profile.draft_synthesis_emotion_scale ?? nextProfiles[existingIndex].draft_synthesis_emotion_scale,
+
+        draft_synthesis_enable_subtitle: profile.draft_synthesis_enable_subtitle ?? nextProfiles[existingIndex].draft_synthesis_enable_subtitle,
+
+        draft_synthesis_silence_duration: profile.draft_synthesis_silence_duration ?? nextProfiles[existingIndex].draft_synthesis_silence_duration,
+
+        draft_synthesis_enable_language_detector: profile.draft_synthesis_enable_language_detector ?? nextProfiles[existingIndex].draft_synthesis_enable_language_detector,
+
+        draft_synthesis_disable_markdown_filter: profile.draft_synthesis_disable_markdown_filter ?? nextProfiles[existingIndex].draft_synthesis_disable_markdown_filter,
+
+        draft_synthesis_disable_emoji_filter: profile.draft_synthesis_disable_emoji_filter ?? nextProfiles[existingIndex].draft_synthesis_disable_emoji_filter,
+
+        draft_synthesis_explicit_language: profile.draft_synthesis_explicit_language ?? nextProfiles[existingIndex].draft_synthesis_explicit_language,
+
+        is_synthesizing: false
+
+      };
+
+      this.voiceProfiles = nextProfiles;
+
+      if (this.voiceSynthesisForm.profileId === nextProfiles[existingIndex].id) {
+
+        this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(nextProfiles[existingIndex]);
+
+      }
+
+      return;
+
+    }
+
+
+
+    this.voiceProfiles = [{ ...profile, is_synthesizing: false }, ...nextProfiles];
+
+    if (!this.voiceSynthesisForm.profileId && !String(this.voiceSynthesisForm.speakerId || '').trim() && this.canSynthesizeVoiceProfile(profile)) {
+
+      this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(profile);
+
+    }
+
+  }
+
+
+
+  private getVoiceCloneModelMeta(modelSource: any): { demoAudio: string; iclSpeakerId: string; xApiResourceId: string; modelVersion: string } {
+
+    const model = Array.isArray(modelSource) ? (modelSource.find((item) => !!item) || {}) : (modelSource || {});
+
+    const rawResourceId = Array.isArray(model?.x_api_resource_id)
+
+      ? model.x_api_resource_id.find((item: any) => String(item || '').trim()) || ''
+
+      : model?.x_api_resource_id;
+
+
+
+    return {
+
+      demoAudio: String(model?.demo_audio || '').trim(),
+
+      iclSpeakerId: String(model?.icl_speaker_id || '').trim(),
+
+      xApiResourceId: String(rawResourceId || '').trim(),
+
+      modelVersion: String(model?.version || model?.model_version || '').trim()
+
+    };
+
+  }
+
+
+
+  private buildVoiceProfileFromCloneApiResponse(response: any, fallback: {
+
+    displayName: string;
+
+    speakerId: string;
+
+    sampleText: string;
+
+    audioText: string;
+
+    sourceAudioName: string;
+
+    sourceAudioFormat: string;
+
+    language: number;
+
+  }): VoiceProfile | null {
+
+    const timbre = response?.data?.timbre;
+
+    if (!timbre || typeof timbre !== 'object') {
+
+      return null;
+
+    }
+
+
+
+    const modelMeta = this.getVoiceCloneModelMeta(timbre?.models);
+
+    return this.hydrateVoiceProfile({
+
+      id: String(timbre?.objectId || `voice-${Date.now()}`),
+
+      name: String(timbre?.name || fallback.displayName || '未命名音色'),
+
+      creation_mode: 'clone',
+
+      speaker_id: String(timbre?.speaker_id || fallback.speakerId || ''),
+
+      timbre_id: String(timbre?.objectId || ''),
+
+      sample_text: fallback.sampleText,
+
+      source_audio_text: fallback.audioText,
+
+      source_audio_name: fallback.sourceAudioName,
+
+      source_audio_format: fallback.sourceAudioFormat,
+
+      language: fallback.language,
+
+      status: timbre?.status === null || timbre?.status === undefined || timbre?.status === '' ? null : Number(timbre.status),
+
+      status_label: String(response?.data?.tip || (modelMeta.demoAudio ? '可用' : '未知')),
+
+      demo_audio: modelMeta.demoAudio,
+
+      occupied: !!timbre?.occupied,
+
+      x_api_resource_id: modelMeta.xApiResourceId,
+
+      model_version: modelMeta.modelVersion,
+
+      icl_speaker_id: modelMeta.iclSpeakerId,
+
+      latest_audio_url: modelMeta.demoAudio,
+
+      message: String(response?.data?.tip || ''),
+
+      created_at: timbre?.createdAt || new Date().toISOString(),
+
+      updated_at: timbre?.updatedAt || new Date().toISOString()
+
+    });
+
+  }
+
+
+
+  private applySynthesisResponseToProfile(profile: VoiceProfile, response: any, payload: {
+
+    text: string;
+
+    ssml: string;
+
+    xApiResourceId: string;
+
+    model: string;
+
+    format: string;
+
+    sampleRate: number;
+
+    speechRate: number;
+
+    loudnessRate: number;
+
+    emotion: string;
+
+    emotionScale: number;
+
+    enableSubtitle: boolean;
+
+    silenceDuration: number;
+
+    enableLanguageDetector: boolean;
+
+    disableMarkdownFilter: boolean;
+
+    disableEmojiFilter: boolean;
+
+    explicitLanguage: string;
+
+  }): VoiceProfile {
+
+    const audioUrl = String(response?.data?.audioUrl || '').trim();
+
+    const workId = String(response?.data?.workId || '').trim();
+
+
+
+    return this.hydrateVoiceProfile({
+
+      ...profile,
+
+      synthesized_audio_url: audioUrl || profile.synthesized_audio_url || '',
+
+      synthesized_work_id: workId || profile.synthesized_work_id || '',
+
+      latest_audio_url: audioUrl || profile.latest_audio_url || profile.demo_audio || '',
+
+      last_synthesis_text: payload.text,
+
+      last_synthesis_ssml: payload.ssml,
+
+      last_synthesis_x_api_resource_id: payload.xApiResourceId || profile.x_api_resource_id || '',
+
+      last_synthesis_model: payload.model,
+
+      last_synthesis_format: payload.format,
+
+      last_synthesis_sample_rate: payload.sampleRate,
+
+      last_synthesis_speech_rate: payload.speechRate,
+
+      last_synthesis_loudness_rate: payload.loudnessRate,
+
+      last_synthesis_emotion: payload.emotion,
+
+      last_synthesis_emotion_scale: payload.emotionScale,
+
+      last_synthesis_enable_subtitle: payload.enableSubtitle,
+
+      last_synthesis_silence_duration: payload.silenceDuration,
+
+      last_synthesis_enable_language_detector: payload.enableLanguageDetector,
+
+      last_synthesis_disable_markdown_filter: payload.disableMarkdownFilter,
+
+      last_synthesis_disable_emoji_filter: payload.disableEmojiFilter,
+
+      last_synthesis_explicit_language: payload.explicitLanguage,
+
+      updated_at: new Date().toISOString()
+
+    });
+
+  }
+
+
+
+  private syncVoiceProfileToBackend(profile: VoiceProfile | null | undefined): void {
+
+    if (!profile?.id) {
+
+      return;
+
+    }
+
+
+
+    this.http.post<any>('/backend/api/voice-profiles/sync', {
+
+      ...profile,
+
+      created_at: profile.created_at instanceof Date ? profile.created_at.toISOString() : profile.created_at,
+
+      updated_at: profile.updated_at instanceof Date ? profile.updated_at.toISOString() : profile.updated_at
+
+    }).subscribe({
+
+      next: (res) => {
+
+        if (res?.profile) {
+
+          this.upsertLocalVoiceProfile(this.hydrateVoiceProfile(res.profile));
+
+          this.refreshView();
+
+        }
+
+      },
+
+      error: (err) => {
+
+        console.warn('⚠️ 音色记录本地同步失败:', err);
+
+      }
+
+    });
+
+  }
+
+
+
+  private isVoiceSpeakerIdentifier(value: string): boolean {
+
+    const normalizedValue = String(value || '').trim();
+
+    return /^(S_|ICL_|icl_|saturn_|dit_)/.test(normalizedValue);
+
+  }
+
+
+
+  onVoiceSourceAudioSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (!input.files || input.files.length === 0) {
+
+      return;
+
+    }
+
+
+
+    const file = input.files[0];
+
+    const ext = (file.name.match(/\.[^.]+$/)?.[0] || '').toLowerCase();
+
+    const allowedTypes = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav', 'audio/mp4', 'audio/x-m4a', 'audio/aac', 'audio/flac', 'audio/ogg', 'audio/opus', 'application/octet-stream'];
+
+    const allowedExts = ['.mp3', '.wav', '.m4a', '.aac', '.flac', '.ogg', '.opus', '.pcm'];
+
+
+
+    if ((!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) || file.size <= 0) {
+
+      this.voiceDesignError = '音频样本仅支持 mp3、wav、m4a、aac、flac、ogg、opus、pcm 格式';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (file.size > 20 * 1024 * 1024) {
+
+      this.voiceDesignError = '音频样本大小不能超过 20MB';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.voiceSourceAudioFile = file;
+
+    this.voiceSourceAudioName = file.name;
+
+    this.voiceDesignError = '';
+
+    input.value = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  clearVoiceSourceAudio(): void {
+
+    this.voiceSourceAudioFile = null;
+
+    this.voiceSourceAudioName = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  onVoiceDesignImageSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (!input.files || input.files.length === 0) {
+
+      return;
+
+    }
+
+
+
+    const file = input.files[0];
+
+    if (!String(file.type || '').startsWith('image/') || file.size <= 0) {
+
+      this.voiceDesignError = '图片提示仅支持常见图片格式';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (file.size > 10 * 1024 * 1024) {
+
+      this.voiceDesignError = '图片提示大小不能超过 10MB';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.voiceDesignImageFile = file;
+
+    this.voiceDesignImageName = file.name;
+
+    this.voiceDesignError = '';
+
+    input.value = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  clearVoiceDesignImage(): void {
+
+    this.voiceDesignImageFile = null;
+
+    this.voiceDesignImageName = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  setVoiceCreateMode(mode: 'clone' | 'design' | 'synthesize'): void {
+
+    if (this.voiceCreateMode === mode) {
+
+      return;
+
+    }
+
+    this.voiceCreateMode = mode;
+
+    this.voiceDesignError = '';
+
+    if (mode === 'synthesize') {
+
+      this.syncVoiceSynthesisFormSelection();
+
+    }
+
+    this.refreshView();
+
+  }
+
+
+
+  private resetVoiceCreateForm(): void {
+
+    this.voiceForm = this.buildDefaultVoiceForm();
+
+    this.voiceSourceAudioFile = null;
+
+    this.voiceSourceAudioName = '';
+
+    this.voiceDesignImageFile = null;
+
+    this.voiceDesignImageName = '';
+
+  }
+
+
+
+  getVoiceProfileAudioUrl(profile: VoiceProfile | null | undefined): string {
+
+    if (!profile) {
+
+      return '';
+
+    }
+
+
+
+    return String(profile.latest_audio_url || profile.synthesized_audio_url || profile.demo_audio || '').trim();
+
+  }
+
+
+
+  canSynthesizeVoiceProfile(profile: VoiceProfile | null | undefined): boolean {
+
+    if (!profile) {
+
+      return false;
+
+    }
+
+
+
+    return !!String(profile.icl_speaker_id || profile.speaker_id || '').trim();
+
+  }
+
+
+
+  getSynthesisVoiceProfiles(): VoiceProfile[] {
+
+    return this.voiceProfiles.filter((profile) => this.canSynthesizeVoiceProfile(profile));
+
+  }
+
+
+
+  getSelectedVoiceSynthesisProfile(): VoiceProfile | null {
+
+    return this.getSynthesisVoiceProfiles().find((profile) => profile.id === this.voiceSynthesisForm.profileId) || null;
+
+  }
+
+
+
+  selectVoiceSynthesisProfile(profileId: string): void {
+
+    if (!profileId) {
+
+      this.voiceSynthesisForm = {
+
+        ...this.voiceSynthesisForm,
+
+        profileId: ''
+
+      };
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    const nextProfile = this.voiceProfiles.find((profile) => profile.id === profileId) || null;
+
+    this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(nextProfile);
+
+    this.refreshView();
+
+  }
+
+
+
+  openVoiceSynthesisWorkbench(profile: VoiceProfile): void {
+
+    if (!this.canSynthesizeVoiceProfile(profile)) {
+
+      this.showToast('❌ 当前音色缺少可用于语音合成的 speaker 标识', 'error');
+
+      return;
+
+    }
+
+
+
+    this.voiceCreateMode = 'synthesize';
+
+    this.voiceDesignError = '';
+
+    this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(profile);
+
+    this.refreshView();
+
+  }
+
+
+
+  getVoiceProfileAudioLabel(profile: VoiceProfile | null | undefined): string {
+
+    if (!profile) {
+
+      return '暂无可用音频';
+
+    }
+
+
+
+    const audioUrl = this.getVoiceProfileAudioUrl(profile);
+
+    if (!audioUrl) {
+
+      return '暂无可用音频';
+
+    }
+
+
+
+    if (audioUrl === String(profile.synthesized_audio_url || '')) {
+
+      return '文本合成音频';
+
+    }
+
+
+
+    if (audioUrl === String(profile.demo_audio || '')) {
+
+      return '试听音频';
+
+    }
+
+
+
+    return '音色音频';
+
+  }
+
+
+
+  createVoiceProfile(): void {
+
+    if (this.isCreatingVoiceProfile) {
+
+      return;
+
+    }
+
+
+
+    if (this.voiceCreateMode === 'synthesize') {
+
+      this.submitVoiceSynthesisForm();
+
+      return;
+
+    }
+
+
+
+    if (this.voiceCreateMode === 'design') {
+
+      this.createVoiceDesignProfile();
+
+      return;
+
+    }
+
+
+
+    this.createVoiceCloneProfile();
+
+  }
+
+
+
+  private createVoiceCloneProfile(): void {
+
+    const speakerId = this.voiceForm.speakerId.trim();
+
+    const displayName = this.voiceForm.displayName.trim();
+
+    const audioText = this.voiceForm.audioText.trim();
+
+    const sampleText = this.voiceForm.sampleText.trim();
+
+    const synthesisText = this.voiceForm.synthesisText.trim();
+
+    const sourceAudioFile = this.voiceSourceAudioFile;
+
+
+
+    if (!displayName) {
+
+      this.voiceDesignError = '请输入音色名称';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!speakerId) {
+
+      this.voiceDesignError = '请输入音色代号 speaker_id';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!sourceAudioFile) {
+
+      this.voiceDesignError = '请上传用于复刻的音频样本';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!sampleText) {
+
+      this.voiceDesignError = '请输入试听文本';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (sampleText.length < 4 || sampleText.length > 80) {
+
+      this.voiceDesignError = '试听文本长度需在 4-80 字之间';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.isCreatingVoiceProfile = true;
+
+    this.voiceDesignError = '';
+
+
+
+    const finalizeCloneFlow = (profile: VoiceProfile | null, toastType: 'success' | 'warn', toastMessage: string) => {
+
+      if (profile) {
+
+        profile.draft_synthesis_text = profile.last_synthesis_text || synthesisText;
+
+        this.upsertLocalVoiceProfile(profile);
+
+        this.syncVoiceProfileToBackend(profile);
+
+        if (this.getVoiceProfileAudioUrl(profile)) {
+
+          this.preferredVoiceProfileId = profile.id;
+
+        }
+
+      }
+
+
+
+      this.resetVoiceCreateForm();
+
+      this.isCreatingVoiceProfile = false;
+
+      this.showToast(toastMessage, toastType, 5000);
+
+      this.refreshView();
+
+    };
+
+
+
+    this.readFileAsBase64(sourceAudioFile).then((audioBase64) => {
+
+      const sourceAudioFormat = this.inferVoiceSourceAudioFormat(sourceAudioFile);
+
+      this.http.post<any>(`${this.voiceTtsBaseUrl}/voice_clone`, {
+
+        token: this.voiceApiToken,
+
+        name: displayName,
+
+        speaker_id: speakerId,
+
+        audioData: {
+
+          base64: audioBase64,
+
+          format: sourceAudioFormat,
+
+          ...(audioText ? { text: audioText } : {})
+
+        },
+
+        language: this.voiceForm.language || 0,
+
+        extra_params: {
+
+          demo_text: sampleText
+
+        }
+
+      }).subscribe({
+
+        next: (res) => {
+
+          const baseProfile: VoiceProfile | null = res?.profile
+
+            ? this.hydrateVoiceProfile(res.profile)
+
+            : this.buildVoiceProfileFromCloneApiResponse(res, {
+
+              displayName,
+
+              speakerId,
+
+              sampleText,
+
+              audioText,
+
+              sourceAudioName: sourceAudioFile.name,
+
+              sourceAudioFormat,
+
+              language: this.voiceForm.language || 0
+
+            });
+
+
+
+          if (!synthesisText || !baseProfile) {
+
+            finalizeCloneFlow(
+
+              baseProfile,
+
+              'success',
+
+              '✅ 音色复刻完成,可在列表中继续合成文本并用于数字人生成'
+
+            );
+
+            return;
+
+          }
+
+
+
+          this.http.post<any>(`${this.voiceTtsBaseUrl}/unidirectional`, {
+
+            token: this.voiceApiToken,
+
+            text: synthesisText,
+
+            timbreId: baseProfile.timbre_id || '',
+
+            speaker_id: baseProfile.icl_speaker_id || baseProfile.speaker_id || '',
+
+            isStream: false,
+
+            x_api_resource_id: baseProfile.x_api_resource_id || ''
+
+          }).subscribe({
+
+            next: (synthesisRes) => {
+
+              const nextProfile: VoiceProfile | null = synthesisRes?.profile
+
+                ? this.hydrateVoiceProfile(synthesisRes.profile)
+
+                : this.applySynthesisResponseToProfile(baseProfile, synthesisRes, {
+
+                  text: synthesisText,
+
+                  ssml: '',
+
+                  xApiResourceId: baseProfile.x_api_resource_id || '',
+
+                  model: '',
+
+                  format: 'mp3',
+
+                  sampleRate: 24000,
+
+                  speechRate: 0,
+
+                  loudnessRate: 0,
+
+                  emotion: '',
+
+                  emotionScale: 4,
+
+                  enableSubtitle: false,
+
+                  silenceDuration: 0,
+
+                  enableLanguageDetector: false,
+
+                  disableMarkdownFilter: false,
+
+                  disableEmojiFilter: false,
+
+                  explicitLanguage: ''
+
+                });
+
+              finalizeCloneFlow(nextProfile, 'success', '✅ 音色复刻完成,并已生成首条文本音频');
+
+            },
+
+            error: () => {
+
+              finalizeCloneFlow(baseProfile, 'warn', '⚠️ 音色复刻完成,但首条文本合成失败,可稍后重试');
+
+            }
+
+          });
+
+        },
+
+        error: (err) => {
+
+          this.isCreatingVoiceProfile = false;
+
+          this.voiceDesignError = this.getErrorText(err, '音色复刻失败,请检查 token、speaker_id 与音频参数');
+
+          this.showToast(`❌ ${this.voiceDesignError}`, 'error');
+
+          this.refreshView();
+
+        }
+
+      });
+
+    }).catch(() => {
+
+      this.isCreatingVoiceProfile = false;
+
+      this.voiceDesignError = '音频文件读取失败,请重新选择音频样本';
+
+      this.showToast(`❌ ${this.voiceDesignError}`, 'error');
+
+      this.refreshView();
+
+    });
+
+  }
+
+
+
+  private createVoiceDesignProfile(): void {
+
+    const speakerId = this.voiceForm.speakerId.trim();
+
+    const displayName = this.voiceForm.displayName.trim();
+
+    const sampleText = this.voiceForm.sampleText.trim();
+
+    const textPrompt = this.voiceForm.textPrompt.trim();
+
+
+
+    if (!displayName) {
+
+      this.voiceDesignError = '请输入音色名称';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!speakerId) {
+
+      this.voiceDesignError = '请输入音色代号 speaker_id';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!sampleText) {
+
+      this.voiceDesignError = '请输入试听文本';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (sampleText.length > 300) {
+
+      this.voiceDesignError = '试听文本不能超过 300 字';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (textPrompt.length > 200) {
+
+      this.voiceDesignError = '文本提示词不能超过 200 字';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!textPrompt && !this.voiceDesignImageFile) {
+
+      this.voiceDesignError = '文本提示词与图片提示不能同时为空';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.isCreatingVoiceProfile = true;
+
+    this.voiceDesignError = '';
+
+
+
+    const formData = new FormData();
+
+    formData.append('speakerId', speakerId);
+
+    formData.append('displayName', displayName);
+
+    formData.append('sampleText', sampleText);
+
+    formData.append('textPrompt', textPrompt);
+
+    formData.append('language', String(this.voiceForm.language || 0));
+
+    if (this.voiceDesignImageFile) {
+
+      formData.append('image', this.voiceDesignImageFile, this.voiceDesignImageFile.name);
+
+    }
+
+
+
+    this.http.post<any>('/backend/api/voice/design', formData).subscribe({
+
+      next: (res) => {
+
+        const profile: VoiceProfile | null = res?.profile
+
+          ? this.hydrateVoiceProfile(res.profile)
+
+          : null;
+
+
+
+        if (profile) {
+
+          this.upsertLocalVoiceProfile(profile);
+
+          if (this.getVoiceProfileAudioUrl(profile)) {
+
+            this.preferredVoiceProfileId = profile.id;
+
+          }
+
+        }
+
+
+
+        this.resetVoiceCreateForm();
+
+        this.isCreatingVoiceProfile = false;
+
+        this.showToast('✅ 音色设计完成,可直接试听并用于数字人生成', 'success', 5000);
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        this.isCreatingVoiceProfile = false;
+
+        this.voiceDesignError = this.getErrorText(err, '音色设计失败,请检查 speaker_id、提示词与图片参数');
+
+        this.showToast(`❌ ${this.voiceDesignError}`, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  submitVoiceSynthesisForm(): void {
+
+    const profile = this.voiceProfiles.find((item) => item.id === this.voiceSynthesisForm.profileId) || null;
+
+    const manualVoiceId = String(this.voiceSynthesisForm.speakerId || '').trim();
+
+    if (!profile && !manualVoiceId) {
+
+      this.voiceDesignError = '请选择已有音色,或直接输入 timbreId / speaker_id';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (!profile) {
+
+      this.voiceDesignError = '';
+
+      if (this.isVoiceSpeakerIdentifier(manualVoiceId)) {
+
+        this.synthesizeVoiceBySpeakerId(manualVoiceId);
+
+        return;
+
+      }
+
+
+
+      const manualProfile = this.hydrateVoiceProfile({
+
+        id: `manual-${manualVoiceId}`,
+
+        name: `手动音色 ${manualVoiceId}`,
+
+        creation_mode: 'manual',
+
+        speaker_id: '',
+
+        timbre_id: manualVoiceId,
+
+        sample_text: '',
+
+        source_audio_text: '',
+
+        source_audio_name: '',
+
+        source_audio_format: '',
+
+        language: 0,
+
+        status: null,
+
+        status_label: '手动输入',
+
+        demo_audio: '',
+
+        x_api_resource_id: String(this.voiceSynthesisForm.xApiResourceId || '').trim(),
+
+        model_version: String(this.voiceSynthesisForm.model || '').trim(),
+
+        icl_speaker_id: '',
+
+        occupied: false,
+
+        created_at: new Date().toISOString(),
+
+        updated_at: new Date().toISOString()
+
+      });
+
+      manualProfile.draft_synthesis_text = this.voiceSynthesisForm.text;
+
+      manualProfile.draft_synthesis_ssml = this.voiceSynthesisForm.ssml;
+
+      manualProfile.draft_synthesis_x_api_resource_id = this.voiceSynthesisForm.xApiResourceId;
+
+      manualProfile.draft_synthesis_model = this.voiceSynthesisForm.model;
+
+      manualProfile.draft_synthesis_format = this.voiceSynthesisForm.format;
+
+      manualProfile.draft_synthesis_sample_rate = this.voiceSynthesisForm.sampleRate;
+
+      manualProfile.draft_synthesis_speech_rate = this.voiceSynthesisForm.speechRate;
+
+      manualProfile.draft_synthesis_loudness_rate = this.voiceSynthesisForm.loudnessRate;
+
+      manualProfile.draft_synthesis_emotion = this.voiceSynthesisForm.emotion;
+
+      manualProfile.draft_synthesis_emotion_scale = this.voiceSynthesisForm.emotionScale;
+
+      manualProfile.draft_synthesis_enable_subtitle = this.voiceSynthesisForm.enableSubtitle;
+
+      manualProfile.draft_synthesis_silence_duration = this.voiceSynthesisForm.silenceDuration;
+
+      manualProfile.draft_synthesis_enable_language_detector = this.voiceSynthesisForm.enableLanguageDetector;
+
+      manualProfile.draft_synthesis_disable_markdown_filter = this.voiceSynthesisForm.disableMarkdownFilter;
+
+      manualProfile.draft_synthesis_disable_emoji_filter = this.voiceSynthesisForm.disableEmojiFilter;
+
+      manualProfile.draft_synthesis_explicit_language = this.voiceSynthesisForm.explicitLanguage;
+
+      this.synthesizeVoiceProfile(manualProfile);
+
+      return;
+
+    }
+
+
+
+    this.voiceSynthesisForm.speakerId = String(profile.icl_speaker_id || profile.speaker_id || '').trim();
+
+    profile.draft_synthesis_text = this.voiceSynthesisForm.text;
+
+    profile.draft_synthesis_ssml = this.voiceSynthesisForm.ssml;
+
+    profile.draft_synthesis_x_api_resource_id = this.voiceSynthesisForm.xApiResourceId;
+
+    profile.draft_synthesis_model = this.voiceSynthesisForm.model;
+
+    profile.draft_synthesis_format = this.voiceSynthesisForm.format;
+
+    profile.draft_synthesis_sample_rate = this.voiceSynthesisForm.sampleRate;
+
+    profile.draft_synthesis_speech_rate = this.voiceSynthesisForm.speechRate;
+
+    profile.draft_synthesis_loudness_rate = this.voiceSynthesisForm.loudnessRate;
+
+    profile.draft_synthesis_emotion = this.voiceSynthesisForm.emotion;
+
+    profile.draft_synthesis_emotion_scale = this.voiceSynthesisForm.emotionScale;
+
+    profile.draft_synthesis_enable_subtitle = this.voiceSynthesisForm.enableSubtitle;
+
+    profile.draft_synthesis_silence_duration = this.voiceSynthesisForm.silenceDuration;
+
+    profile.draft_synthesis_enable_language_detector = this.voiceSynthesisForm.enableLanguageDetector;
+
+    profile.draft_synthesis_disable_markdown_filter = this.voiceSynthesisForm.disableMarkdownFilter;
+
+    profile.draft_synthesis_disable_emoji_filter = this.voiceSynthesisForm.disableEmojiFilter;
+
+    profile.draft_synthesis_explicit_language = this.voiceSynthesisForm.explicitLanguage;
+
+    this.voiceDesignError = '';
+
+    this.synthesizeVoiceProfile(profile);
+
+  }
+
+
+
+  private synthesizeVoiceBySpeakerId(speakerId: string): void {
+
+    if (this.isDirectVoiceSynthesizing) {
+
+      return;
+
+    }
+
+
+
+    const text = String(this.voiceSynthesisForm.text || '').trim();
+
+    const ssml = String(this.voiceSynthesisForm.ssml || '').trim();
+
+    if (!text && !ssml) {
+
+      this.voiceDesignError = '请输入要合成的文本内容或 SSML';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    const xApiResourceId = String(this.voiceSynthesisForm.xApiResourceId || '').trim();
+
+    const model = String(this.voiceSynthesisForm.model || '').trim();
+
+    const format = this.normalizeVoiceSynthesisFormat(this.voiceSynthesisForm.format);
+
+    const sampleRate = this.normalizeVoiceSynthesisSampleRate(this.voiceSynthesisForm.sampleRate);
+
+    const speechRate = this.clampVoiceSynthesisRate(this.voiceSynthesisForm.speechRate, 0);
+
+    const loudnessRate = this.clampVoiceSynthesisRate(this.voiceSynthesisForm.loudnessRate, 0);
+
+    const emotion = String(this.voiceSynthesisForm.emotion || '').trim();
+
+    const emotionScale = this.clampVoiceSynthesisEmotionScale(this.voiceSynthesisForm.emotionScale, 4);
+
+    const enableSubtitle = !!this.voiceSynthesisForm.enableSubtitle;
+
+    const silenceDuration = this.clampVoiceSynthesisSilenceDuration(this.voiceSynthesisForm.silenceDuration, 0);
+
+    const explicitLanguage = String(this.voiceSynthesisForm.explicitLanguage || '').trim();
+
+
+
+    this.voiceSynthesisForm = {
+
+      ...this.voiceSynthesisForm,
+
+      speakerId,
+
+      text,
+
+      ssml,
+
+      xApiResourceId,
+
+      model,
+
+      format,
+
+      sampleRate,
+
+      speechRate,
+
+      loudnessRate,
+
+      emotion,
+
+      emotionScale,
+
+      enableSubtitle,
+
+      silenceDuration,
+
+      explicitLanguage
+
+    };
+
+    this.voiceDesignError = '';
+
+    this.isDirectVoiceSynthesizing = true;
+
+    this.directVoiceSynthesisAudioUrl = '';
+
+    this.directVoiceSynthesisWorkId = '';
+
+    this.directVoiceSynthesisSpeakerId = speakerId;
+
+    this.refreshView();
+
+
+
+    this.http.post<any>(`${this.localVoiceTtsBaseUrl}/unidirectional`, {
+
+      token: this.voiceApiToken,
+
+      speaker_id: speakerId,
+
+      text,
+
+      ssml,
+
+      isStream: false,
+
+      x_api_resource_id: xApiResourceId,
+
+      model,
+
+      audio_params: {
+
+        format,
+
+        sample_rate: sampleRate,
+
+        speech_rate: speechRate,
+
+        loudness_rate: loudnessRate,
+
+        emotion,
+
+        emotion_scale: emotionScale,
+
+        enable_subtitle: enableSubtitle
+
+      },
+
+      additions: {
+
+        silence_duration: silenceDuration,
+
+        enable_language_detector: !!this.voiceSynthesisForm.enableLanguageDetector,
+
+        disable_markdown_filter: !!this.voiceSynthesisForm.disableMarkdownFilter,
+
+        disable_emoji_filter: !!this.voiceSynthesisForm.disableEmojiFilter,
+
+        ...(explicitLanguage ? { explicit_language: explicitLanguage } : {})
+
+      }
+
+    }).subscribe({
+
+      next: (res) => {
+
+        this.isDirectVoiceSynthesizing = false;
+
+        this.directVoiceSynthesisAudioUrl = String(res?.data?.audioUrl || '').trim();
+
+        this.directVoiceSynthesisWorkId = String(res?.data?.workId || '').trim();
+
+        this.directVoiceSynthesisSpeakerId = String(res?.speakerId || speakerId).trim();
+
+        if (res?.profile) {
+
+          const nextProfile = this.hydrateVoiceProfile(res.profile);
+
+          this.upsertLocalVoiceProfile(nextProfile);
+
+          this.syncVoiceProfileToBackend(nextProfile);
+
+        } else {
+
+          const matchedProfile = this.voiceProfiles.find((item) => (
+
+            String(item.icl_speaker_id || '').trim() === speakerId
+
+            || String(item.speaker_id || '').trim() === speakerId
+
+          ));
+
+          if (matchedProfile) {
+
+            const nextProfile = this.applySynthesisResponseToProfile(matchedProfile, res, {
+
+              text,
+
+              ssml,
+
+              xApiResourceId,
+
+              model,
+
+              format,
+
+              sampleRate,
+
+              speechRate,
+
+              loudnessRate,
+
+              emotion,
+
+              emotionScale,
+
+              enableSubtitle,
+
+              silenceDuration,
+
+              enableLanguageDetector: !!this.voiceSynthesisForm.enableLanguageDetector,
+
+              disableMarkdownFilter: !!this.voiceSynthesisForm.disableMarkdownFilter,
+
+              disableEmojiFilter: !!this.voiceSynthesisForm.disableEmojiFilter,
+
+              explicitLanguage
+
+            });
+
+            this.upsertLocalVoiceProfile(nextProfile);
+
+            this.syncVoiceProfileToBackend(nextProfile);
+
+          }
+
+        }
+
+        this.showToast(`✅ 已使用 speaker_id「${this.directVoiceSynthesisSpeakerId}」生成语音`, 'success');
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        this.isDirectVoiceSynthesizing = false;
+
+        this.voiceDesignError = this.getErrorText(err, '文本合成失败');
+
+        this.showToast(`❌ ${this.voiceDesignError}`, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  synthesizeVoiceProfile(profile: VoiceProfile): void {
+
+    if (profile.is_synthesizing) {
+
+      return;
+
+    }
+
+
+
+    if (!this.canSynthesizeVoiceProfile(profile)) {
+
+      this.showToast('❌ 该音色记录缺少可用于语音合成的 speaker 标识,暂时无法进行文本合成', 'error');
+
+      return;
+
+    }
+
+
+
+    const text = String(profile.draft_synthesis_text || '').trim();
+
+    const ssml = String(profile.draft_synthesis_ssml || '').trim();
+
+    if (!text && !ssml) {
+
+      this.showToast('❌ 请输入要合成的文本内容或 SSML', 'error');
+
+      return;
+
+    }
+
+
+
+    const xApiResourceId = String(profile.draft_synthesis_x_api_resource_id || profile.x_api_resource_id || '').trim();
+
+    const model = String(profile.draft_synthesis_model || '').trim();
+
+    const format = this.normalizeVoiceSynthesisFormat(profile.draft_synthesis_format);
+
+    const sampleRate = this.normalizeVoiceSynthesisSampleRate(profile.draft_synthesis_sample_rate);
+
+    const speechRate = this.clampVoiceSynthesisRate(profile.draft_synthesis_speech_rate, 0);
+
+    const loudnessRate = this.clampVoiceSynthesisRate(profile.draft_synthesis_loudness_rate, 0);
+
+    const emotion = String(profile.draft_synthesis_emotion || '').trim();
+
+    const emotionScale = this.clampVoiceSynthesisEmotionScale(profile.draft_synthesis_emotion_scale, 4);
+
+    const enableSubtitle = !!profile.draft_synthesis_enable_subtitle;
+
+    const silenceDuration = this.clampVoiceSynthesisSilenceDuration(profile.draft_synthesis_silence_duration, 0);
+
+    const explicitLanguage = String(profile.draft_synthesis_explicit_language || '').trim();
+
+
+
+    profile.is_synthesizing = true;
+
+    this.refreshView();
+
+
+
+    this.http.post<any>(`${this.voiceTtsBaseUrl}/unidirectional`, {
+
+      token: this.voiceApiToken,
+
+      text,
+
+      ssml,
+
+      timbreId: profile.timbre_id || '',
+
+      speaker_id: profile.icl_speaker_id || profile.speaker_id || '',
+
+      isStream: false,
+
+      x_api_resource_id: xApiResourceId,
+
+      model,
+
+      audio_params: {
+
+        format,
+
+        sample_rate: sampleRate,
+
+        speech_rate: speechRate,
+
+        loudness_rate: loudnessRate,
+
+        emotion,
+
+        emotion_scale: emotionScale,
+
+        enable_subtitle: enableSubtitle
+
+      },
+
+      additions: {
+
+        silence_duration: silenceDuration,
+
+        enable_language_detector: !!profile.draft_synthesis_enable_language_detector,
+
+        disable_markdown_filter: !!profile.draft_synthesis_disable_markdown_filter,
+
+        disable_emoji_filter: !!profile.draft_synthesis_disable_emoji_filter,
+
+        ...(explicitLanguage ? { explicit_language: explicitLanguage } : {})
+
+      }
+
+    }).subscribe({
+
+      next: (res) => {
+
+        const nextProfile = res?.profile
+
+          ? this.hydrateVoiceProfile(res.profile)
+
+          : this.applySynthesisResponseToProfile(profile, res, {
+
+            text,
+
+            ssml,
+
+            xApiResourceId,
+
+            model,
+
+            format,
+
+            sampleRate,
+
+            speechRate,
+
+            loudnessRate,
+
+            emotion,
+
+            emotionScale,
+
+            enableSubtitle,
+
+            silenceDuration,
+
+            enableLanguageDetector: !!profile.draft_synthesis_enable_language_detector,
+
+            disableMarkdownFilter: !!profile.draft_synthesis_disable_markdown_filter,
+
+            disableEmojiFilter: !!profile.draft_synthesis_disable_emoji_filter,
+
+            explicitLanguage
+
+          });
+
+        nextProfile.draft_synthesis_text = text;
+
+        nextProfile.draft_synthesis_ssml = ssml;
+
+        nextProfile.draft_synthesis_x_api_resource_id = xApiResourceId;
+
+        nextProfile.draft_synthesis_model = model;
+
+        nextProfile.draft_synthesis_format = format;
+
+        nextProfile.draft_synthesis_sample_rate = sampleRate;
+
+        nextProfile.draft_synthesis_speech_rate = speechRate;
+
+        nextProfile.draft_synthesis_loudness_rate = loudnessRate;
+
+        nextProfile.draft_synthesis_emotion = emotion;
+
+        nextProfile.draft_synthesis_emotion_scale = emotionScale;
+
+        nextProfile.draft_synthesis_enable_subtitle = enableSubtitle;
+
+        nextProfile.draft_synthesis_silence_duration = silenceDuration;
+
+        nextProfile.draft_synthesis_enable_language_detector = !!profile.draft_synthesis_enable_language_detector;
+
+        nextProfile.draft_synthesis_disable_markdown_filter = !!profile.draft_synthesis_disable_markdown_filter;
+
+        nextProfile.draft_synthesis_disable_emoji_filter = !!profile.draft_synthesis_disable_emoji_filter;
+
+        nextProfile.draft_synthesis_explicit_language = explicitLanguage;
+
+        this.upsertLocalVoiceProfile(nextProfile);
+
+        this.syncVoiceProfileToBackend(nextProfile);
+
+        if (this.voiceSynthesisForm.profileId === nextProfile.id) {
+
+          this.voiceSynthesisForm = this.buildVoiceSynthesisFormFromProfile(nextProfile);
+
+        }
+
+        if (this.getVoiceProfileAudioUrl(nextProfile)) {
+
+          this.preferredVoiceProfileId = nextProfile.id;
+
+        }
+
+        this.showToast(`✅ 已为「${nextProfile.name}」生成文本音频`, 'success');
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        profile.is_synthesizing = false;
+
+        this.showToast(`❌ ${this.getErrorText(err, '文本合成失败')}`, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  removeVoiceProfile(profile: VoiceProfile): void {
+
+    if (!confirm(`确定要删除音色「${profile.name}」吗?`)) {
+
+      return;
+
+    }
+
+
+
+    this.http.delete<any>(`/backend/voice-profiles/${profile.id}`).subscribe({
+
+      next: () => {
+
+        this.voiceProfiles = this.voiceProfiles.filter(item => item.id !== profile.id);
+
+        this.syncVoiceSynthesisFormSelection();
+
+        if (this.preferredVoiceProfileId === profile.id) {
+
+          this.preferredVoiceProfileId = '';
+
+        }
+
+        this.showToast(`🗑️ 已删除音色「${profile.name}」`, 'success');
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        this.showToast(`❌ ${this.getErrorText(err, '删除音色失败')}`, 'error');
+
+      }
+
+    });
+
+  }
+
+
+
+  onRemixDigitalHumanImageSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (!input.files || input.files.length === 0) {
+
+      return;
+
+    }
+
+
+
+    const file = input.files[0];
+
+    const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/gif'];
+
+    const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+
+    const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
+
+
+
+    if ((!allowedTypes.includes(file.type) && !allowedExts.includes(ext)) || file.size <= 0) {
+
+      this.remixDigitalHumanError = '仅支持 jpg、jpeg、png、gif 格式的形象图';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    if (file.size > 20 * 1024 * 1024) {
+
+      this.remixDigitalHumanError = '形象图大小不能超过 20MB';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.revokeDigitalHumanPreviewUrl();
+
+    this.remixDigitalHumanImageFile = file;
+
+    this.remixDigitalHumanImageName = file.name;
+
+    this.remixDigitalHumanImagePreviewUrl = URL.createObjectURL(file);
+
+    this.remixDigitalHumanImageAssetUrl = '';
+
+    this.remixDigitalHumanError = '';
+
+    input.value = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  clearRemixDigitalHumanImage(): void {
+
+    this.revokeDigitalHumanPreviewUrl();
+
+    this.remixDigitalHumanImageFile = null;
+
+    this.remixDigitalHumanImageName = '';
+
+    this.remixDigitalHumanImageAssetUrl = '';
+
+    this.remixDigitalHumanError = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  goToRemixGenerationConfig(): void {
+
+    if (this.remixMode === 'digital-human') {
+
+      if (!this.remixDigitalHumanImageFile) {
+
+        this.remixDigitalHumanError = '请先上传数字人形象图';
+
+        this.refreshView();
+
+        return;
+
+      }
+
+
+
+      this.remixStep = 4;
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.confirmTranscript();
+
+  }
+
+
+
+  private revokeDigitalHumanPreviewUrl(): void {
+
+    if (this.remixDigitalHumanImagePreviewUrl) {
+
+      URL.revokeObjectURL(this.remixDigitalHumanImagePreviewUrl);
+
+      this.remixDigitalHumanImagePreviewUrl = '';
+
+    }
+
+  }
+
+
+
+  private resetDigitalHumanState(): void {
+
+    this.revokeDigitalHumanPreviewUrl();
+
+    this.remixDigitalHumanImageFile = null;
+
+    this.remixDigitalHumanImageName = '';
+
+    this.remixDigitalHumanImageAssetUrl = '';
+
+    this.remixDigitalHumanPrompt = '';
+
+    this.remixDigitalHumanFastMode = false;
+
+    this.remixDigitalHumanResolution = '1080p';
+
+    this.remixDigitalHumanError = '';
+
+    this.remixDigitalHumanAudioSource = 'tts';
+
+    this.remixDigitalHumanTtsText = '';
+
+    this.remixDigitalHumanTtsTimbreId = '';
+
+    this.remixDigitalHumanTtsSynthesizing = false;
+
+  }
+
+
+
+  // 加载该视频关联的 Whisper 处理文件(通过后端读取)
+
+  loadWhisperFiles(video: ManagedVideo): void {
+
+    if (!video.whisper) {
+
+      console.log('ℹ️ 该视频无 Whisper 转录数据,跳过自动加载');
+
+      return;
+
+    }
+
+
+
+    const w = video.whisper;
+
+
+
+    // 优先加载美化文字稿,其次原始转录
+
+    const transcriptPath = w.beautified || w.transcript;
+
+    if (transcriptPath) {
+
+      this.http.get(`/backend/files/read`, { params: { path: transcriptPath }, responseType: 'text' }).subscribe({
+
+        next: (text) => {
+
+          this.remixTranscript = text;
+
+          if (w.beautified) this.remixBeautifiedTranscript = text;
+
+          console.log(`📄 已加载 Whisper 文字稿: ${transcriptPath}`);
+
+        },
+
+        error: () => console.log(`⚠️ 未能加载文字稿: ${transcriptPath}`)
+
+      });
+
+    }
+
+
+
+    // 加载分镜脚本
+
+    if (w.storyboard) {
+
+      this.http.get<any>(`/backend/files/read`, { params: { path: w.storyboard } }).subscribe({
+
+        next: (data: any) => {
+
+          if (Array.isArray(data)) {
+
+            this.remixStoryboard = data.map((seg: any) => ({
+
+              id: seg.id,
+
+              act: seg.act,
+
+              narration: seg.narration,
+
+              prompt: seg.prompt,
+
+              duration: seg.duration,
+
+              status: 'pending' as const
+
+            }));
+
+            console.log(`📝 已加载分镜脚本: ${this.remixStoryboard.length} 个片段`);
+
+          }
+
+        },
+
+        error: () => console.log(`⚠️ 未能加载分镜脚本: ${w.storyboard}`)
+
+      });
+
+    }
+
+  }
+
+
+
+  // 下一步
+
+  nextRemixStep(): void {
+
+    if (this.remixStep < 6) this.remixStep++;
+
+  }
+
+
+
+  // 上一步
+
+  prevRemixStep(): void {
+
+    if (this.remixStep > 1 && !this.isRemixSubmitting) this.remixStep--;
+
+  }
+
+
+
+  // 使用 Whisper 生成文字稿(通过本地后端)
+
+  generateTranscript(): void {
+
+    if (!this.remixTargetVideo || this.isWhisperRunning) return;
+
+
+
+    const video = this.remixTargetVideo;
+
+    this.isWhisperRunning = true;
+
+    this.whisperStatusText = '正在检查 Whisper 服务...';
+
+
+
+    // 先检查后端是否可用
+
+    this.http.get<any>('/backend/api/health').subscribe({
+
+      next: () => {
+
+        // 后端可用,调用 Whisper 转录
+
+        this.whisperStatusText = '正在调用 Whisper 语音识别,视频较长时可能需要数分钟...';
+
+
+
+        this.http.post<any>('/backend/api/whisper/transcribe', {
+
+          videoId: video.id,
+
+          language: 'Chinese',
+
+          model: 'base'
+
+        }).subscribe({
+
+          next: (res: any) => {
+
+            if (res?.success && res?.transcript) {
+
+              this.remixTranscript = res.transcript;
+
+              this.whisperStatusText = '✅ 识别完成!';
+
+              this.isWhisperRunning = false;
+
+
+
+              // 后端已自动更新 manifest,同步前端的 whisper 字段
+
+              if (res.whisper) {
+
+                video.whisper = res.whisper;
+
+              }
+
+              console.log('✅ Whisper 转录完成,manifest 已更新');
+
+            } else {
+
+              this.whisperStatusText = '识别完成但未返回文字稿';
+
+              this.isWhisperRunning = false;
+
+              this.showManualTranscriptInput = true;
+
+            }
+
+          },
+
+          error: (err) => {
+
+            console.error('Whisper 转录失败:', err);
+
+            const msg = err?.error?.error || err?.error?.hint || '转录过程出错';
+
+            this.whisperStatusText = `❌ ${msg}`;
+
+            this.isWhisperRunning = false;
+
+            this.showManualTranscriptInput = true;
+
+          }
+
+        });
+
+      },
+
+      error: () => {
+
+        // 后端不可用,降级为手动方案
+
+        this.onWhisperFallback(video);
+
+      }
+
+    });
+
+  }
+
+
+
+  // 后端不可用时的降级方案
+
+  private onWhisperFallback(video: ManagedVideo): void {
+
+    this.isWhisperRunning = false;
+
+    this.whisperStatusText = '';
+
+    const cmd = `whisper "data/videos/${video.filename}" --model base --language Chinese --output_dir "Whisper"`;
+
+
+
+    navigator.clipboard.writeText(cmd).then(() => {
+
+      alert(
+
+        `后端服务未启动。请先在终端运行:\n\nnpm run server\n\n` +
+
+        `或手动执行 Whisper 命令(已复制到剪贴板):\n\n${cmd}`
+
+      );
+
+    }).catch(() => {
+
+      alert(`后端服务未启动。请先运行 npm run server,或手动执行:\n\n${cmd}`);
+
+    });
+
+    this.showManualTranscriptInput = true;
+
+  }
+
+
+
+  // Step 1 → 2: 确认文字稿,进入AI美化
+
+  confirmTranscript(): void {
+
+    if (!this.remixTranscript.trim()) return;
+
+    this.remixStep = 2;
+
+    // 自动执行AI美化
+
+    this.beautifyTranscript();
+
+  }
+
+
+
+  // Step 2: AI美化文字稿(当前为本地处理,后续可接入LLM API)
+
+  beautifyTranscript(): void {
+
+    let text = this.remixTranscript;
+
+    // 去除口语化词汇
+
+    const fillerWords = ['嗯', '啊', '呢', '吧', '哦', '额', '那个', '就是说', '然后呢', '对吧'];
+
+    fillerWords.forEach(w => { text = text.replace(new RegExp(w, 'g'), ''); });
+
+    // 去除多余空格和空行
+
+    text = text.replace(/\s+/g, ' ').trim();
+
+    // 按句号/问号/感叹号分句,重新组织段落
+
+    const sentences = text.split(/(?<=[。!?.!?])/g).filter(s => s.trim());
+
+    const paragraphs: string[] = [];
+
+    let current = '';
+
+    sentences.forEach(s => {
+
+      current += s.trim();
+
+      if (current.length >= 60) {
+
+        paragraphs.push(current);
+
+        current = '';
+
+      }
+
+    });
+
+    if (current) paragraphs.push(current);
+
+    this.remixBeautifiedTranscript = paragraphs.join('\n\n');
+
+    if (!this.remixBeautifiedTranscript.trim()) {
+
+      this.remixBeautifiedTranscript = this.remixTranscript;
+
+    }
+
+  }
+
+
+
+  // Step 2 → 3: 确认美化文字稿,生成分镜脚本
+
+  confirmBeautified(): void {
+
+    if (!this.remixBeautifiedTranscript.trim()) return;
+
+    this.generateStoryboard();
+
+    this.remixStep = 3;
+
+  }
+
+
+
+  // Step 3: 根据美化文字稿生成分镜脚本(参考SKILL.md Stage 1)
+
+  generateStoryboard(): void {
+
+    const text = this.remixBeautifiedTranscript;
+
+    // 按约40-45个中文字(≈15秒语速)拆分为分镜段
+
+    const segmentCharLen = 42;
+
+    const segments: StoryboardSegment[] = [];
+
+    // 优先按段落分割
+
+    const paragraphs = text.split(/\n+/).filter(p => p.trim());
+
+    let charBuffer = '';
+
+    let segIndex = 0;
+
+
+
+    const pushSegment = (narration: string) => {
+
+      segIndex++;
+
+      const id = `S-${String(segIndex).padStart(2, '0')}`;
+
+      const act = segIndex === 1 ? '开场' : (segIndex <= 3 ? '引入' : '正文');
+
+      const stylePrompt = this.remixStyleOptions.find(s => s.id === this.remixStyle)?.prompt || '';
+
+      // 生成英文提示词:风格 + 场景描述
+
+      const prompt = `${stylePrompt}, cinematic scene depicting: ${narration.substring(0, 80)}`;
+
+      segments.push({
+
+        id,
+
+        act,
+
+        narration: narration.trim(),
+
+        prompt,
+
+        duration: this.remixFrames === 121 ? '5s' : '10s',
+
+        status: 'pending'
+
+      });
+
+    };
+
+
+
+    paragraphs.forEach(para => {
+
+      charBuffer += para;
+
+      while (charBuffer.length >= segmentCharLen) {
+
+        // 尝试在句子边界切割
+
+        let cutPos = segmentCharLen;
+
+        const searchRange = charBuffer.substring(0, segmentCharLen + 15);
+
+        const punctPos = Math.max(
+
+          searchRange.lastIndexOf('。'),
+
+          searchRange.lastIndexOf('!'),
+
+          searchRange.lastIndexOf('?'),
+
+          searchRange.lastIndexOf(','),
+
+          searchRange.lastIndexOf(';')
+
+        );
+
+        if (punctPos > segmentCharLen * 0.5) cutPos = punctPos + 1;
+
+        pushSegment(charBuffer.substring(0, cutPos));
+
+        charBuffer = charBuffer.substring(cutPos);
+
+      }
+
+    });
+
+    // 剩余部分
+
+    if (charBuffer.trim().length > 0) {
+
+      pushSegment(charBuffer);
+
+    }
+
+
+
+    this.remixStoryboard = segments;
+
+    console.log(`📝 分镜脚本生成完毕:${segments.length} 个片段`);
+
+  }
+
+
+
+  // Step 3 → 4: 确认分镜,进入参数配置
+
+  confirmStoryboard(): void {
+
+    if (this.remixStoryboard.length === 0) return;
+
+    this.remixStep = 4;
+
+  }
+
+
+
+  // Step 4 → 5: 开始批量生成
+
+  startBatchGeneration(): void {
+
+    if (this.isRemixSubmitting) return;
+
+
+
+    if (this.remixMode === 'digital-human') {
+
+      this.startDigitalHumanGeneration();
+
+      return;
+
+    }
+
+
+
+    if (this.remixStoryboard.length === 0) return;
+
+
+
+    const video = this.remixTargetVideo!;
+
+    const styleName = this.remixStyleOptions.find(s => s.id === this.remixStyle)?.name || this.remixStyle;
+
+
+
+    this.isRemixSubmitting = true;
+
+    this.remixStep = 5;
+
+    video.isRemixing = true;
+
+    video.remixProgress = 0;
+
+    video.remixStyle = styleName;
+
+    this.remixResults = { total: this.remixStoryboard.length, success: 0, failed: 0 };
+
+    this.remixStatusText = '正在提交生成任务...';
+
+    this.currentRemixId = `REMIX-${Date.now()}`;
+
+
+
+    // 创建持久化任务
+
+    const newTask: Task = {
+
+      id: `TASK-${Date.now()}`,
+
+      keyword: `${video.title} - ${styleName}`,
+
+      status: 'processing',
+
+      step: 'generate',
+
+      progress: 0,
+
+      cost: 0,
+
+      created_at: new Date(),
+
+      updated_at: new Date(),
+
+      original_video_title: video.title,
+
+      duration: `${this.remixStoryboard.length} 个片段`
+
+    };
+
+    this.activeTasks.unshift(newTask);
+
+    this.currentRemixTaskId = newTask.id;
+
+    this.saveTask(newTask);
+
+
+
+    // 显示 toast 并关闭模态框
+
+    this.showToast(`任务已提交,可在「任务管理」中查看进度`, 'success');
+
+    this.showRemixModal = false;
+
+
+
+    console.log(`🎨 开始批量生成 ${this.remixStoryboard.length} 个分镜视频, 任务ID: ${newTask.id}`);
+
+
+
+    // 逐个提交(串行,避免频率限制)
+
+    this.generateSegmentSequentially(0, video, styleName);
+
+  }
+
+
+
+  private startDigitalHumanGeneration(): void {
+
+    if (!this.remixTargetVideo || !this.remixDigitalHumanImageFile || this.isRemixSubmitting) {
+
+      return;
+
+    }
+
+
+
+    const video = this.remixTargetVideo;
+
+    const isTtsMode = this.remixDigitalHumanAudioSource === 'tts';
+
+
+
+    if (!isTtsMode) {
+
+      const safeDuration = Number(video.duration || 0);
+
+      if (safeDuration > 60) {
+
+        this.remixDigitalHumanError = '原视频音频时长超过 60 秒,当前数字人接口不支持';
+
+        this.showToast(`❌ ${this.remixDigitalHumanError}`, 'error');
+
+        this.refreshView();
+
+        return;
+
+      }
+
+    }
+
+
+
+    if (isTtsMode) {
+
+      if (!this.remixDigitalHumanTtsTimbreId.trim()) {
+
+        this.remixDigitalHumanError = '请输入或选择音色 ID(可在「语音合成」标签页中复刻获取)';
+
+        this.refreshView();
+
+        return;
+
+      }
+
+      if (!this.remixDigitalHumanTtsText.trim()) {
+
+        this.remixDigitalHumanError = '请输入要合成的语音文本';
+
+        this.refreshView();
+
+        return;
+
+      }
+
+    }
+
+
+
+    const styleName = '数字人生成';
+
+    this.isRemixSubmitting = true;
+
+    this.remixStep = 5;
+
+    video.isRemixing = true;
+
+    video.remixProgress = 0;
+
+    video.remixStyle = styleName;
+
+    this.remixResults = { total: 1, success: 0, failed: 0 };
+
+    this.remixStatusText = '正在准备数字人素材...';
+
+    this.remixStitchStatus = 'idle';
+
+    this.remixStitchError = '';
+
+    this.remixFinalVideoUrl = '';
+
+    this.currentRemixId = `REMIX-${Date.now()}`;
+
+
+
+    const newTask: Task = {
+
+      id: `TASK-${Date.now()}`,
+
+      keyword: `${video.title} - ${styleName}`,
+
+      status: 'processing',
+
+      step: 'generate',
+
+      progress: 0,
+
+      cost: 0,
+
+      created_at: new Date(),
+
+      updated_at: new Date(),
+
+      original_video_title: video.title,
+
+      duration: '数字人视频'
+
+    };
+
+    this.activeTasks.unshift(newTask);
+
+    this.currentRemixTaskId = newTask.id;
+
+    this.saveTask(newTask);
+
+
+
+    this.showToast('数字人任务已提交,可在「任务管理」中查看进度', 'success');
+
+    this.showRemixModal = false;
+
+
+
+    this.jimengService.uploadFileToParse(
+
+      this.remixDigitalHumanImageFile,
+
+      `digital-human-${Date.now()}-${this.remixDigitalHumanImageFile.name}`,
+
+      this.remixDigitalHumanImageFile.type || 'image/jpeg'
+
+    ).subscribe({
+
+      next: (imageUrl: string) => {
+
+        this.remixDigitalHumanImageAssetUrl = imageUrl;
+
+        video.remixProgress = 10;
+
+        this.updateRemixTaskProgress('generate', 10);
+
+
+
+        if (isTtsMode) {
+
+          this.remixStatusText = '形象图上传完成,正在合成语音...';
+
+          this.remixDigitalHumanTtsSynthesizing = true;
+
+          this.refreshView();
+
+
+
+          this.http.post<any>('https://server.fmode.cn/api/volcengine/tts/unidirectional', {
+
+            token: 'Bearer r:f0333969e312a40e4703e8fe4ed1c600',
+
+            text: this.remixDigitalHumanTtsText.trim(),
+
+            timbreId: this.remixDigitalHumanTtsTimbreId.trim(),
+
+            isStream: false,
+
+            audio_params: {
+
+              format: 'mp3',
+
+              sample_rate: 24000,
+
+              speech_rate: 0,
+
+              loudness_rate: 0
+
+            }
+
+          }).subscribe({
+
+            next: (res) => {
+
+              this.remixDigitalHumanTtsSynthesizing = false;
+
+              if (res?.code === 200 && res?.data?.audioUrl) {
+
+                video.remixProgress = 30;
+
+                this.remixStatusText = '语音合成完成,正在识别数字人主体...';
+
+                this.updateRemixTaskProgress('generate', 30);
+
+                this.startDigitalHumanGenerationWithAudio(video, imageUrl, res.data.audioUrl);
+
+              } else {
+
+                this.handleDigitalHumanGenerationError(video, res?.error?.message || '语音合成失败,请检查音色ID和文本');
+
+              }
+
+            },
+
+            error: (err) => {
+
+              this.remixDigitalHumanTtsSynthesizing = false;
+
+              this.handleDigitalHumanGenerationError(video, this.getErrorText(err, '语音合成请求失败'));
+
+            }
+
+          });
+
+        } else {
+
+          this.remixStatusText = '形象图上传完成,正在提取原视频音频...';
+
+          this.refreshView();
+
+
+
+          this.http.post('/backend/api/remix/extract-audio', {
+
+            videoId: video.id
+
+          }, {
+
+            responseType: 'blob'
+
+          }).subscribe({
+
+            next: (audioBlob: Blob) => {
+
+              const audioFile = new File(
+
+                [audioBlob],
+
+                `${video.filename.replace(/\.[^.]+$/, '') || 'source-audio'}.wav`,
+
+                { type: audioBlob.type || 'audio/wav' }
+
+              );
+
+              video.remixProgress = 20;
+
+              this.remixStatusText = '音频提取完成,正在上传音频素材...';
+
+              this.updateRemixTaskProgress('generate', 20);
+
+
+
+              this.jimengService.uploadFileToParse(audioFile, audioFile.name, audioFile.type).subscribe({
+
+                next: (audioUrl: string) => {
+
+                  video.remixProgress = 30;
+
+                  this.remixStatusText = '素材准备完成,正在识别数字人主体...';
+
+                  this.updateRemixTaskProgress('generate', 30);
+
+                  this.startDigitalHumanGenerationWithAudio(video, imageUrl, audioUrl);
+
+                },
+
+                error: (err) => {
+
+                  this.handleDigitalHumanGenerationError(video, err?.message || '上传音频素材失败');
+
+                }
+
+              });
+
+            },
+
+            error: async (err) => {
+
+              const message = await this.extractHttpBlobErrorMessage(err, '提取原视频音频失败,请确认已安装 ffmpeg');
+
+              this.handleDigitalHumanGenerationError(video, message);
+
+            }
+
+          });
+
+        }
+
+      },
+
+      error: (err) => {
+
+        this.handleDigitalHumanGenerationError(video, err?.message || '上传数字人形象图失败');
+
+      }
+
+    });
+
+  }
+
+
+
+  private startDigitalHumanGenerationWithAudio(video: ManagedVideo, imageUrl: string, audioUrl: string): void {
+
+    this.jimengService.identifyDigitalHuman(imageUrl).subscribe({
+
+      next: ({ workId }) => {
+
+        this.jimengService.pollDigitalHumanIdentifyUntilReady(workId, (status, progress) => {
+
+          const mappedProgress = Math.min(30 + progress * 0.2, 50);
+
+          video.remixProgress = mappedProgress;
+
+          this.remixStatusText = status;
+
+          this.updateRemixTaskProgress('generate', mappedProgress);
+
+        }).subscribe({
+
+          next: () => {
+
+            this.remixStatusText = '主体识别完成,开始生成数字人视频...';
+
+            video.remixProgress = 50;
+
+            this.updateRemixTaskProgress('generate', 50);
+
+
+
+            this.jimengService.generateDigitalHuman(workId, audioUrl, {
+
+              prompt: this.remixDigitalHumanPrompt,
+
+              peFastMode: this.remixDigitalHumanResolution === '720p' ? true : this.remixDigitalHumanFastMode,
+
+              outputResolution: this.remixDigitalHumanResolution,
+
+              maskUrl: []
+
+            }, (status, progress) => {
+
+              const mappedProgress = Math.min(50 + progress * 0.45, 95);
+
+              video.remixProgress = mappedProgress;
+
+              this.remixStatusText = status;
+
+              this.updateRemixTaskProgress('generate', mappedProgress);
+
+            }).subscribe({
+
+              next: (result) => {
+
+                video.remixProgress = 100;
+
+                this.remixResults.success = 1;
+
+                this.remixFinalVideoUrl = result.videoUrl;
+
+                this.remixStatusText = '数字人视频生成完成';
+
+                this.createDigitalHumanVersion(video, result.videoUrl);
+
+                this.completeRemixTask('completed', result.videoUrl);
+
+                this.saveRemixSession('completed');
+
+                video.isRemixing = false;
+
+                this.isRemixSubmitting = false;
+
+                this.remixStep = 6;
+
+                this.showToast(`✅ 数字人视频已生成:${video.title}`, 'success', 6000);
+
+                this.refreshView();
+
+              },
+
+              error: (err) => {
+
+                this.handleDigitalHumanGenerationError(video, err?.message || '数字人生成失败');
+
+              }
+
+            });
+
+          },
+
+          error: (err) => {
+
+            this.handleDigitalHumanGenerationError(video, err?.message || '数字人主体识别失败');
+
+          }
+
+        });
+
+      },
+
+      error: (err) => {
+
+        this.handleDigitalHumanGenerationError(video, err?.message || '提交数字人主体识别失败');
+
+      }
+
+    });
+
+  }
+
+
+
+  private handleDigitalHumanGenerationError(video: ManagedVideo, message: string): void {
+
+    video.isRemixing = false;
+
+    video.remixProgress = 0;
+
+    this.isRemixSubmitting = false;
+
+    this.remixResults.failed = 1;
+
+    this.remixStep = 6;
+
+    this.remixStatusText = message;
+
+    this.remixStitchStatus = 'failed';
+
+    this.remixStitchError = message;
+
+    this.completeRemixTask('failed', undefined, message);
+
+    this.saveRemixSession('failed');
+
+    this.showToast(`❌ ${message}`, 'error');
+
+    this.refreshView();
+
+  }
+
+
+
+  // 串行生成每个分镜段的视频
+
+  private generateSegmentSequentially(index: number, video: ManagedVideo, styleName: string): void {
+
+    if (index >= this.remixStoryboard.length) {
+
+      // 全部完成
+
+      this.onBatchGenerationComplete(video, styleName);
+
+      return;
+
+    }
+
+
+
+    const segment = this.remixStoryboard[index];
+
+    segment.status = 'generating';
+
+    const totalSegments = this.remixStoryboard.length;
+
+    this.remixStatusText = `正在生成第 ${index + 1}/${totalSegments} 个片段...`;
+
+    video.remixProgress = (index / totalSegments) * 100;
+
+
+
+    // 更新任务进度
+
+    this.updateRemixTaskProgress('generate', (index / totalSegments) * 80);
+
+
+
+    this.jimengService.remixVideo(
+
+      segment.prompt,
+
+      {
+
+        method: '1',
+
+        frames: this.remixFrames,
+
+        aspectRatio: this.remixAspectRatio,
+
+        quality: this.remixQuality
+
+      },
+
+      (status: string, progress: number) => {
+
+        // 单片段内进度映射到总进度
+
+        const segProgress = (index + progress / 100) / totalSegments * 100;
+
+        video.remixProgress = segProgress;
+
+        this.remixStatusText = `片段 ${index + 1}/${totalSegments}: ${status}`;
+
+      }
+
+    ).subscribe({
+
+      next: (result) => {
+
+        segment.status = 'completed';
+
+        segment.videoUrl = result.videoUrl;
+
+        segment.workId = result.workId;
+
+        this.remixResults.success++;
+
+
+
+        // 添加到生成结果
+
+        this.generatedResults.unshift({
+
+          id: `REMIX-${Date.now()}-${segment.id}`,
+
+          type: 'video',
+
+          url: result.videoUrl,
+
+          title: `${video.title} - ${styleName} [${segment.id}]`,
+
+          created_at: new Date(),
+
+          quality: this.remixQuality === 'pro' ? '1080P Pro' : '720P',
+
+          duration: this.remixFrames === 121 ? '5秒' : '10秒',
+
+          cost: this.remixQuality === 'pro' ? 5.0 : 1.4
+
+        });
+
+
+
+        console.log(`✅ 片段 ${segment.id} 完成: ${result.videoUrl}`);
+
+        // 自动保存当前会话
+
+        this.saveRemixSession('generating');
+
+        // 延迟1秒再提交下一个(避免频率限制)
+
+        setTimeout(() => this.generateSegmentSequentially(index + 1, video, styleName), 1000);
+
+      },
+
+      error: (err) => {
+
+        segment.status = 'failed';
+
+        segment.error = err.message || '生成失败';
+
+        this.remixResults.failed++;
+
+        console.error(`❌ 片段 ${segment.id} 失败:`, err);
+
+        this.saveRemixSession('generating');
+
+        // 失败也继续下一个
+
+        setTimeout(() => this.generateSegmentSequentially(index + 1, video, styleName), 1000);
+
+      }
+
+    });
+
+  }
+
+
+
+  // 批量生成完成 → 自动触发一键成片拼接
+
+  private onBatchGenerationComplete(video: ManagedVideo, styleName: string): void {
+
+    this.remixStep = 6;
+
+    this.remixStatusText = `片段生成完成:成功 ${this.remixResults.success}/${this.remixResults.total}`;
+
+    console.log(`🎉 批量生成完成: 成功${this.remixResults.success}, 失败${this.remixResults.failed}`);
+
+
+
+    // 更新任务进度到80%
+
+    this.updateRemixTaskProgress('generate', 80);
+
+
+
+    // 收集所有成功片段的视频URL
+
+    const successUrls = this.remixStoryboard
+
+      .filter(s => s.status === 'completed' && s.videoUrl)
+
+      .map(s => s.videoUrl!);
+
+
+
+    if (successUrls.length < 1) {
+
+      video.isRemixing = false;
+
+      video.remixProgress = 100;
+
+      this.isRemixSubmitting = false;
+
+      this.remixStitchStatus = 'failed';
+
+      this.remixStitchError = '没有成功的视频片段可以拼接';
+
+      this.remixStatusText = `生成完成,但没有可拼接的片段`;
+
+      this.completeRemixTask('failed', undefined, '所有片段生成失败');
+
+      this.saveRemixSession('failed');
+
+      this.showToast('任务失败:所有片段生成失败', 'error');
+
+      return;
+
+    }
+
+
+
+    // 一键成片功能暂时关闭(Quickly API 凭证过期),直接标记完成
+
+    // TODO: 待 Quickly 凭证更新后恢复自动拼接功能
+
+    this.remixStitchStatus = 'failed';
+
+    this.remixStitchError = '一键成片功能暂不可用(Quickly API 凭证过期),各片段可单独查看/下载';
+
+    this.remixStatusText = `片段生成完成:成功 ${successUrls.length} 个,一键成片暂不可用`;
+
+    video.isRemixing = false;
+
+    video.remixProgress = 100;
+
+    this.isRemixSubmitting = false;
+
+
+
+    const firstUrl = successUrls[0];
+
+    this.createRemixVersion(video, this.remixStyle, styleName, firstUrl);
+
+    this.completeRemixTask('completed', firstUrl);
+
+    this.saveRemixSession('completed');
+
+    this.showToast(`✅ AI重塑完成!「${video.title}」共 ${successUrls.length} 个片段`, 'success', 6000);
+
+    console.log(`✅ AI重塑完成(一键成片已跳过): ${successUrls.length} 个片段`);
+
+  }
+
+
+
+  // 结果页点击完成
+
+  finishRemix(): void {
+
+    this.showRemixModal = false;
+
+    this.remixTargetVideo = null;
+
+    this.resetDigitalHumanState();
+
+    this.setCurrentTab('results');
+
+  }
+
+
+
+  private createDigitalHumanVersion(video: ManagedVideo, videoUrl: string): void {
+
+    const remixVersion: RemixVersion = {
+
+      id: `REMIX-${Date.now()}`,
+
+      style: '数字人生成',
+
+      created_at: new Date(),
+
+      file_path: videoUrl,
+
+      thumbnail: this.remixDigitalHumanImageAssetUrl || video.thumbnail || 'assets/default-remix-thumb.jpg',
+
+      description: '使用上传形象图与原视频提取音频生成的数字人版本',
+
+      parameters: {
+
+        style: 'digital-human',
+
+        intensity: 1,
+
+        preserve_audio: true,
+
+        quality: this.remixDigitalHumanResolution === '1080p' ? 'high' : 'standard',
+
+        effects: ['digital-human']
+
+      }
+
+    };
+
+
+
+    if (!video.remixVersions) {
+
+      video.remixVersions = [];
+
+    }
+
+    video.remixVersions.unshift(remixVersion);
+
+
+
+    const durationSeconds = Math.min(Number(video.duration || 0) || 0, 60);
+
+    const estimatedCost = Math.round(durationSeconds * 100) / 100;
+
+    const qualityLabel = this.remixDigitalHumanResolution === '1080p' ? '1080P' : '720P';
+
+
+
+    this.saveResult({
+
+      type: 'video',
+
+      url: videoUrl,
+
+      title: `${video.title} - 数字人生成`,
+
+      quality: qualityLabel,
+
+      duration: durationSeconds > 0 ? `${durationSeconds} 秒` : '数字人视频',
+
+      cost: estimatedCost,
+
+      remixId: this.currentRemixId,
+
+      videoId: video.id,
+
+      segments: [{ id: 'DH-01', videoUrl, prompt: this.remixDigitalHumanPrompt, status: 'completed' }]
+
+    });
+
+
+
+    this.saveHistoryRecord({
+
+      keyword: `${video.title} - 数字人生成`,
+
+      original_video_title: video.title,
+
+      status: 'completed',
+
+      cost: estimatedCost,
+
+      duration: durationSeconds > 0 ? `${durationSeconds} 秒` : '数字人视频',
+
+      remixId: this.currentRemixId,
+
+      videoId: video.id,
+
+      styleName: '数字人生成',
+
+      resultUrl: videoUrl,
+
+      quality: qualityLabel,
+
+      segmentCount: 1,
+
+      successCount: 1
+
+    });
+
+  }
+
+
+
+  // 创建重塑版本 + 持久化结果和历史
+
+  private createRemixVersion(video: ManagedVideo, styleId: string, styleName: string, videoUrl?: string): void {
+
+    const remixVersion: RemixVersion = {
+
+      id: `REMIX-${Date.now()}`,
+
+      style: styleName,
+
+      created_at: new Date(),
+
+      file_path: videoUrl || `remix/${video.filename.replace('.mp4', `-${styleId}.mp4`)}`,
+
+      thumbnail: video.thumbnail || 'assets/default-remix-thumb.jpg',
+
+      description: `使用${styleName}风格重塑的视频版本`,
+
+      parameters: {
+
+        style: styleId,
+
+        intensity: 0.8,
+
+        preserve_audio: true,
+
+        quality: this.remixQuality === 'pro' ? 'ultra' : 'high',
+
+        effects: [styleId, 'enhancement']
+
+      }
+
+    };
+
+
+
+    if (!video.remixVersions) {
+
+      video.remixVersions = [];
+
+    }
+
+    video.remixVersions.unshift(remixVersion);
+
+
+
+    // 收集所有成功片段数据
+
+    const successSegments = this.remixStoryboard
+
+      .filter(s => s.status === 'completed' && s.videoUrl)
+
+      .map(s => ({ id: s.id, videoUrl: s.videoUrl!, prompt: s.prompt, status: s.status }));
+
+
+
+    // 计算费用估算
+
+    const costPerSecond = this.remixQuality === 'pro' ? 1.0 : this.remixQuality === '1080p' ? 0.63 : 0.28;
+
+    const estimatedCost = Math.round(this.remixResults.success * 5 * costPerSecond * 100) / 100;
+
+
+
+    // 质量显示
+
+    const qualityLabel = this.remixQuality === 'pro' ? 'Pro 1080P' : this.remixQuality === '1080p' ? '1080P' : '720P';
+
+
+
+    // 持久化:保存生成结果(包含所有片段)
+
+    this.saveResult({
+
+      type: 'video',
+
+      url: videoUrl || '',
+
+      title: `${video.title} - ${styleName}风格重塑`,
+
+      quality: qualityLabel,
+
+      duration: `${this.remixResults.success} 个片段`,
+
+      cost: estimatedCost,
+
+      remixId: this.currentRemixId,
+
+      videoId: video.id,
+
+      segments: successSegments
+
+    });
+
+
+
+    // 持久化:保存历史记录
+
+    this.saveHistoryRecord({
+
+      keyword: `${video.title} - ${styleName}`,
+
+      original_video_title: video.title,
+
+      status: this.remixResults.success > 0 ? 'completed' : 'failed',
+
+      cost: estimatedCost,
+
+      duration: `${this.remixResults.success}/${this.remixResults.total} 片段`,
+
+      remixId: this.currentRemixId,
+
+      videoId: video.id,
+
+      styleName: styleName,
+
+      resultUrl: videoUrl || successSegments[0]?.videoUrl || '',
+
+      quality: qualityLabel,
+
+      segmentCount: this.remixResults.total,
+
+      successCount: this.remixResults.success
+
+    });
+
+
+
+    console.log('重塑版本已创建:', remixVersion);
+
+  }
+
+
+
+  // 查看重塑版本
+
+  viewRemixVersions(video: ManagedVideo): void {
+
+    if (!video.remixVersions || video.remixVersions.length === 0) {
+
+      alert('该视频暂无重塑版本');
+
+      return;
+
+    }
+
+
+
+    console.log('查看重塑版本:', video.remixVersions);
+
+
+
+    let versionsList = `「${video.title}」的重塑版本:\n\n`;
+
+    video.remixVersions.forEach((version, index) => {
+
+      versionsList += `${index + 1}. ${version.style} - ${version.description}\n   创建时间: ${version.created_at.toLocaleString()}\n\n`;
+
+    });
+
+
+
+    alert(versionsList);
+
+  }
+
+
+
+  // 获取重塑状态文本
+
+  getRemixStatusText(video: ManagedVideo): string {
+
+    if (video.isRemixing && video.remixProgress !== undefined) {
+
+      return `重塑中... ${Math.round(video.remixProgress)}%`;
+
+    }
+
+    return 'AI重塑';
+
+  }
+
+
+
+  // 获取重塑状态颜色
+
+  getRemixStatusColor(video: ManagedVideo): string {
+
+    if (video.isRemixing) {
+
+      return 'btn-accent';
+
+    }
+
+    return 'btn-primary';
+
+  }
+
+
+
+  // ==================== 语音合成标签页方法 ====================
+
+
+
+  setVsTtsMode(mode: 'clone' | 'synthesize'): void {
+
+    this.vsTtsMode = mode;
+
+    this.vsCloneError = '';
+
+    this.vsSynthError = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  onVsCloneAudioSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (!input.files || input.files.length === 0) return;
+
+
+
+    const file = input.files[0];
+
+    const allowedExts = ['.mp3', '.wav', '.m4a', '.ogg', '.aac', '.flac', '.wma'];
+
+    const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+
+    if (!allowedExts.includes(ext) || file.size <= 0) {
+
+      this.vsCloneError = '仅支持 mp3、wav、m4a、ogg、aac、flac、wma 格式的音频文件';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (file.size > 50 * 1024 * 1024) {
+
+      this.vsCloneError = '音频文件不能超过 50MB';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.vsCloneAudioFile = file;
+
+    this.vsCloneAudioName = file.name;
+
+    this.vsCloneAudioUrl = '';
+
+    this.vsCloneError = '';
+
+    input.value = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  clearVsCloneAudio(): void {
+
+    this.vsCloneAudioFile = null;
+
+    this.vsCloneAudioName = '';
+
+    this.vsCloneAudioUrl = '';
+
+    this.vsCloneError = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  private vsUploadCloneAudio(): Promise<string> {
+
+    return new Promise((resolve, reject) => {
+
+      if (!this.vsCloneAudioFile) {
+
+        reject(new Error('未选择音频文件'));
+
+        return;
+
+      }
+
+      if (this.vsCloneAudioUrl) {
+
+        resolve(this.vsCloneAudioUrl);
+
+        return;
+
+      }
+
+
+
+      this.vsCloneUploading = true;
+
+      this.refreshView();
+
+
+
+      const formData = new FormData();
+
+      const filename = `voice-clone-${Date.now()}-${this.vsCloneAudioFile.name}`;
+
+      formData.append('file', this.vsCloneAudioFile, filename);
+
+
+
+      this.http.post<any>('/backend/api/remix/upload-asset', formData).subscribe({
+
+        next: (res) => {
+
+          this.vsCloneUploading = false;
+
+          const url = res?.url || '';
+
+          if (!url) {
+
+            reject(new Error('上传音频失败,未返回可用 URL'));
+
+            return;
+
+          }
+
+          this.vsCloneAudioUrl = url;
+
+          this.refreshView();
+
+          resolve(url);
+
+        },
+
+        error: (err) => {
+
+          this.vsCloneUploading = false;
+
+          this.refreshView();
+
+          reject(new Error(this.getErrorText(err, '音频上传失败')));
+
+        }
+
+      });
+
+    });
+
+  }
+
+
+
+  vsStartClone(): void {
+
+    if (this.vsCloneLoading || this.vsCloneUploading) return;
+
+
+
+    const { name, speakerId, audioText, language, demoText } = this.vsCloneForm;
+
+    if (!name.trim()) {
+
+      this.vsCloneError = '请输入音色名称';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (!speakerId.trim()) {
+
+      this.vsCloneError = '请选择 speaker_id';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (!this.vsCloneAudioFile) {
+
+      this.vsCloneError = '请先上传音频文件';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (!demoText.trim() || demoText.trim().length < 4 || demoText.trim().length > 80) {
+
+      this.vsCloneError = '试听文本长度需在 4-80 字之间';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.vsCloneLoading = true;
+
+    this.vsCloneError = '';
+
+    this.vsCloneResult = null;
+
+    this.refreshView();
+
+
+
+    this.vsUploadCloneAudio().then((audioUrl) => {
+
+      this.http.post<any>('https://server.fmode.cn/api/volcengine/tts/voice_clone', {
+
+        token: 'Bearer r:f0333969e312a40e4703e8fe4ed1c600',
+
+        name: name.trim(),
+
+        speaker_id: speakerId.trim(),
+
+        audioData: {
+
+          url: audioUrl,
+
+          ...(audioText.trim() ? { text: audioText.trim() } : {})
+
+        },
+
+        language: language,
+
+        extra_params: {
+
+          demo_text: demoText.trim()
+
+        }
+
+      }).subscribe({
+
+        next: (res) => {
+
+          this.vsCloneLoading = false;
+
+          this.vsCloneResult = res;
+
+          if (res?.code === 200 && res?.data?.timbre) {
+
+            const timbre = res.data.timbre;
+
+            const existing = this.vsClonedTimbreList.find(t => t.objectId === timbre.objectId);
+
+            if (!existing) {
+
+              this.vsClonedTimbreList.unshift({
+
+                objectId: timbre.objectId,
+
+                name: timbre.name || name.trim(),
+
+                speakerId: timbre.speaker_id || speakerId.trim()
+
+              });
+
+              this.saveClonedTimbreList();
+
+            }
+
+            if (!this.vsSynthForm.timbreId) {
+
+              this.vsSynthForm.timbreId = timbre.objectId;
+
+            }
+
+            this.showToast(`✅ 音色「${timbre.name}」复刻成功`, 'success');
+
+          } else {
+
+            this.vsCloneError = res?.error?.message || '复刻失败,请检查参数';
+
+            this.showToast(`❌ ${this.vsCloneError}`, 'error');
+
+          }
+
+          this.refreshView();
+
+        },
+
+        error: (err) => {
+
+          this.vsCloneLoading = false;
+
+          this.vsCloneError = this.getErrorText(err, '音色复刻请求失败');
+
+          this.showToast(`❌ ${this.vsCloneError}`, 'error');
+
+          this.refreshView();
+
+        }
+
+      });
+
+    }).catch((uploadErr) => {
+
+      this.vsCloneLoading = false;
+
+      this.vsCloneError = uploadErr?.message || '音频上传失败';
+
+      this.showToast(`❌ ${this.vsCloneError}`, 'error');
+
+      this.refreshView();
+
+    });
+
+  }
+
+
+
+  vsStartSynthesize(): void {
+
+    if (this.vsSynthLoading) return;
+
+
+
+    const { timbreId, text, isStream, format, sampleRate, speechRate, loudnessRate } = this.vsSynthForm;
+
+    if (!timbreId.trim()) {
+
+      this.vsSynthError = '请输入音色 ID(timbreId),可先通过音色复刻获取';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (!text.trim()) {
+
+      this.vsSynthError = '请输入要合成的文本内容';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    this.vsSynthLoading = true;
+
+    this.vsSynthError = '';
+
+    this.vsSynthAudioUrl = '';
+
+    this.vsSynthWorkId = '';
+
+    this.refreshView();
+
+
+
+    this.http.post<any>('https://server.fmode.cn/api/volcengine/tts/unidirectional', {
+
+      token: 'Bearer r:f0333969e312a40e4703e8fe4ed1c600',
+
+      text: text.trim(),
+
+      timbreId: timbreId.trim(),
+
+      isStream: false,
+
+      audio_params: {
+
+        format: format || 'mp3',
+
+        sample_rate: sampleRate || 24000,
+
+        speech_rate: speechRate || 0,
+
+        loudness_rate: loudnessRate || 0
+
+      }
+
+    }).subscribe({
+
+      next: (res) => {
+
+        this.vsSynthLoading = false;
+
+        if (res?.code === 200 && res?.data?.audioUrl) {
+
+          this.vsSynthAudioUrl = res.data.audioUrl;
+
+          this.vsSynthWorkId = res.data.workId || '';
+
+          this.showToast('✅ 语音合成成功', 'success');
+
+        } else {
+
+          this.vsSynthError = res?.error?.message || '合成失败,请检查参数';
+
+          this.showToast(`❌ ${this.vsSynthError}`, 'error');
+
+        }
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        this.vsSynthLoading = false;
+
+        this.vsSynthError = this.getErrorText(err, '语音合成请求失败');
+
+        this.showToast(`❌ ${this.vsSynthError}`, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  vsSelectClonedTimbre(objectId: string): void {
+
+    this.vsSynthForm.timbreId = objectId;
+
+    this.vsTtsMode = 'synthesize';
+
+    this.vsSynthError = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  // ==================== 数字人合成独立页面 ====================
+
+
+
+  onDhImageSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (!input.files || input.files.length === 0) return;
+
+    const file = input.files[0];
+
+    const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
+
+    const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+
+    if (!allowedExts.includes(ext)) {
+
+      this.dhError = '仅支持 jpg、jpeg、png、gif 格式的形象图';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (file.size > 20 * 1024 * 1024) {
+
+      this.dhError = '形象图大小不能超过 20MB';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    this.dhRevokeImagePreview();
+
+    this.dhImageFile = file;
+
+    this.dhImageName = file.name;
+
+    this.dhImagePreviewUrl = URL.createObjectURL(file);
+
+    this.dhImageAssetUrl = '';
+
+    this.dhError = '';
+
+    input.value = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  clearDhImage(): void {
+
+    this.dhRevokeImagePreview();
+
+    this.dhImageFile = null;
+
+    this.dhImageName = '';
+
+    this.dhImageAssetUrl = '';
+
+    this.dhError = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  private dhRevokeImagePreview(): void {
+
+    if (this.dhImagePreviewUrl) {
+
+      URL.revokeObjectURL(this.dhImagePreviewUrl);
+
+      this.dhImagePreviewUrl = '';
+
+    }
+
+  }
+
+
+
+  onDhAudioSelected(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    if (!input.files || input.files.length === 0) return;
+
+    const file = input.files[0];
+
+    const allowedExts = ['.mp3', '.wav', '.m4a', '.ogg', '.aac', '.flac', '.wma'];
+
+    const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
+
+    if (!allowedExts.includes(ext)) {
+
+      this.dhError = '仅支持 MP3、WAV、M4A、OGG、AAC、FLAC、WMA 格式的音频';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    if (file.size > 50 * 1024 * 1024) {
+
+      this.dhError = '音频文件大小不能超过 50MB';
+
+      input.value = '';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+    this.dhUploadAudioFile = file;
+
+    this.dhUploadAudioName = file.name;
+
+    this.dhUploadAudioUrl = '';
+
+    this.dhError = '';
+
+    input.value = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  clearDhAudio(): void {
+
+    this.dhUploadAudioFile = null;
+
+    this.dhUploadAudioName = '';
+
+    this.dhUploadAudioUrl = '';
+
+    this.dhError = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  dhReset(): void {
+
+    this.dhRevokeImagePreview();
+
+    this.dhStep = 'form';
+
+    this.dhImageFile = null;
+
+    this.dhImageName = '';
+
+    this.dhImageAssetUrl = '';
+
+    this.dhAudioSource = 'tts';
+
+    this.dhTtsTimbreId = '';
+
+    this.dhTtsText = '';
+
+    this.dhUploadAudioFile = null;
+
+    this.dhUploadAudioName = '';
+
+    this.dhUploadAudioUrl = '';
+
+    this.dhUploadingAudio = false;
+
+    this.dhPrompt = '';
+
+    this.dhFastMode = false;
+
+    this.dhResolution = '1080p';
+
+    this.dhError = '';
+
+    this.dhLoading = false;
+
+    this.dhStatusText = '';
+
+    this.dhProgress = 0;
+
+    this.dhResultVideoUrl = '';
+
+    this.dhSynthesizing = false;
+
+    this.refreshView();
+
+  }
+
+
+
+  // 数字人形象库:从生成结果中筛选数字人作品
+
+  getDigitalHumanResults(): GeneratedResult[] {
+
+    return (this.generatedResults || []).filter(r => {
+
+      const title = String(r?.title || '');
+
+      return title.includes('数字人合成') || title.includes('数字人');
+
+    });
+
+  }
+
+
+
+  // ========================================================================
+
+  //  数字人合成页:AI 对话助手 —— 让用户用自然语言要求 AI 改写口播稿 / 优化提示词
+
+  // ========================================================================
+
+
+
+  /** 发送一条对话给 AI,AI 基于当前 dhTtsText / dhPrompt 给出修改建议 */
+
+  dhChatSend(): void {
+
+    const userMsg = (this.dhChatInput || '').trim();
+
+    if (!userMsg || this.dhChatLoading) return;
+
+
+
+    this.dhChatMessages.push({ role: 'user', content: userMsg });
+
+    this.dhChatInput = '';
+
+    this.dhChatLoading = true;
+
+    this.dhChatError = '';
+
+    this.refreshView();
+
+
+
+    const systemPrompt = `你是一位短视频数字人口播脚本编辑助手。你需要根据用户的自然语言指令,在不偏离原意的前提下改写「口播文本」或「补充提示词」。
+
+【当前口播文本】
+
+${this.dhTtsText || '(空)'}
+
+【当前补充提示词】
+
+${this.dhPrompt || '(空)'}
+
+【输出格式】请只输出严格 JSON:
+
+{
+
+  "explanation": "用一两句话向用户解释你做了什么修改(≤40字)",
+
+  "ttsScript": "新的口播文本;若用户本轮指令不需要改写口播,请返回空字符串",
+
+  "dhPrompt": "新的补充提示词;若用户本轮指令不需要改写提示词,请返回空字符串"
+
+}
+
+要求:
+
+- 字符串内不得出现真实换行,必须用 \\n 转义。
+
+- 口播文本控制在 200 字以内,第一人称、自然口语。
+
+- 补充提示词控制在 80 字以内,描述动作 / 镜头语言 / 风格。
+
+- 只返回纯 JSON,禁止 markdown 代码块、禁止前后缀。`;
+
+
+
+    // 把最近若干轮历史拼成一段上下文,避免 token 爆炸
+
+    const history = this.dhChatMessages
+
+      .slice(-8, -1) // 不含本次刚 push 的用户消息
+
+      .map(m => `${m.role === 'user' ? '用户' : 'AI'}: ${m.content}`)
+
+      .join('\n');
+
+    const userInput = (history ? `【历史对话】\n${history}\n\n` : '') + `【本轮用户指令】\n${userMsg}`;
+
+
+
+    console.log('🗣️ [dhChatSend] 发送对话:', { userMsg, ttsLen: this.dhTtsText.length, promptLen: this.dhPrompt.length });
+
+    this.llmService.askWithSystem(systemPrompt, userInput, { model: 'gemini-2.5-flash', max_tokens: 2048 })
+
+      .pipe(
+
+        timeout(60000),
+
+        // ★ finalize 保证不论 next/error/throw/cancel,都强制解锁按钮并刷新视图
+
+        finalize(() => {
+
+          this.dhChatLoading = false;
+
+          this.refreshView();
+
+        })
+
+      )
+
+      .subscribe({
+
+      next: (text) => {
+
+        console.log('🗣️ [dhChatSend] 收到回复,长度:', (text || '').length);
+
+        const cleaned = (text || '').replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/i, '').trim();
+
+        const parsed: any = this.parseRewriteJson(cleaned);
+
+        if (parsed) {
+
+          const explanation = typeof parsed.explanation === 'string' ? parsed.explanation.trim() : '';
+
+          const tts = typeof parsed.ttsScript === 'string' ? parsed.ttsScript.trim() : '';
+
+          const prompt = typeof parsed.dhPrompt === 'string' ? parsed.dhPrompt.trim() : '';
+
+          this.dhChatMessages.push({
+
+            role: 'assistant',
+
+            content: explanation || (tts || prompt ? '已为你生成修改建议,可点击下方按钮应用' : '我没有给出修改'),
+
+            suggestedTts: tts || undefined,
+
+            suggestedPrompt: prompt || undefined,
+
+          });
+
+        } else {
+
+          // 兜底:把原文当作 assistant 文本呈现,不自动应用
+
+          this.dhChatMessages.push({ role: 'assistant', content: cleaned || '(AI 无返回)' });
+
+        }
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        console.error('🗣️ [dhChatSend] 请求失败:', err);
+
+        this.dhChatLoading = false;
+
+        // 优先抽取上游具体错误(fmode 网关返回的 "无效的令牌..." 等)
+
+        const upstream = err?.error?.error?.message || err?.error?.message || err?.error?.error || err?.error || '';
+
+        const msg = (typeof upstream === 'string' && upstream) || err?.message || 'AI 对话请求失败';
+
+        this.dhChatError = msg;
+
+        this.dhChatMessages.push({ role: 'assistant', content: `⚠️ ${msg}` });
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  /** 应用 AI 建议到对应字段;field 控制只应用一项还是两项一起 */
+
+  dhChatApply(msgIndex: number, field: 'tts' | 'prompt' | 'both'): void {
+
+    const msg = this.dhChatMessages[msgIndex];
+
+    if (!msg || msg.role !== 'assistant') return;
+
+
+
+    if ((field === 'tts' || field === 'both') && msg.suggestedTts) {
+
+      this.dhTtsText = msg.suggestedTts.slice(0, 600);
+
+      msg.appliedTts = true;
+
+    }
+
+    if ((field === 'prompt' || field === 'both') && msg.suggestedPrompt) {
+
+      this.dhPrompt = msg.suggestedPrompt.slice(0, 200);
+
+      msg.appliedPrompt = true;
+
+    }
+
+    this.showToast('✅ 已应用 AI 建议', 'success', 2000);
+
+    this.refreshView();
+
+  }
+
+
+
+  /** 清空对话历史 */
+
+  dhChatClear(): void {
+
+    this.dhChatMessages = [];
+
+    this.dhChatError = '';
+
+    this.dhChatInput = '';
+
+    this.refreshView();
+
+  }
+
+
+
+  /** 快捷指令:一键发送预设的改写诉求 */
+
+  dhChatQuickAsk(preset: string): void {
+
+    if (this.dhChatLoading) return;
+
+    this.dhChatInput = preset;
+
+    this.dhChatSend();
+
+  }
+
+
+
+  /** 刷新数字人形象库(重新拉取生成结果) */
+
+  refreshDigitalHumanGallery(): void {
+
+    this.http.get<any[]>('/backend/api/results').subscribe({
+
+      next: (results) => {
+
+        this.generatedResults = (results || []).map(r => ({ ...r, created_at: new Date(r.created_at) }));
+
+        this.showToast('🔄 形象库已刷新', 'success', 2500);
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        this.showToast('❌ 刷新失败,请稍后重试', 'error');
+
+      }
+
+    });
+
+  }
+
+
+
+  dhStartGeneration(): void {
+
+    if (this.dhLoading) return;
+
+
+
+    // 验证形象图
+
+    if (!this.dhImageFile) {
+
+      this.dhError = '请先上传数字人形象图';
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    // 验证音频来源
+
+    if (this.dhAudioSource === 'tts') {
+
+      if (!this.dhTtsTimbreId.trim()) {
+
+        this.dhError = '请输入或选择音色 ID';
+
+        this.refreshView();
+
+        return;
+
+      }
+
+      if (!this.dhTtsText.trim()) {
+
+        this.dhError = '请输入口播文本';
+
+        this.refreshView();
+
+        return;
+
+      }
+
+    } else {
+
+      if (!this.dhUploadAudioFile) {
+
+        this.dhError = '请上传音频文件';
+
+        this.refreshView();
+
+        return;
+
+      }
+
+    }
+
+
+
+    this.dhError = '';
+
+    this.dhLoading = true;
+
+    this.dhStep = 'generating';
+
+    this.dhProgress = 0;
+
+    this.dhStatusText = '正在上传形象图...';
+
+    this.dhResultVideoUrl = '';
+
+    this.refreshView();
+
+
+
+    // 上传形象图
+
+    this.jimengService.uploadFileToParse(
+
+      this.dhImageFile,
+
+      `digital-human-${Date.now()}-${this.dhImageFile.name}`,
+
+      this.dhImageFile.type || 'image/jpeg'
+
+    ).subscribe({
+
+      next: (imageUrl: string) => {
+
+        this.dhImageAssetUrl = imageUrl;
+
+        this.dhProgress = 10;
+
+        this.refreshView();
+
+
+
+        if (this.dhAudioSource === 'tts') {
+
+          this.dhStatusText = '形象图上传完成,正在合成语音...';
+
+          this.dhSynthesizing = true;
+
+          this.refreshView();
+
+
+
+          this.http.post<any>('https://server.fmode.cn/api/volcengine/tts/unidirectional', {
+
+            token: this.voiceApiToken,
+
+            text: this.dhTtsText.trim(),
+
+            timbreId: this.dhTtsTimbreId.trim(),
+
+            isStream: false,
+
+            audio_params: {
+
+              format: 'mp3',
+
+              sample_rate: 24000,
+
+              speech_rate: 0,
+
+              loudness_rate: 0
+
+            }
+
+          }).subscribe({
+
+            next: (res) => {
+
+              this.dhSynthesizing = false;
+
+              if (res?.code === 200 && res?.data?.audioUrl) {
+
+                this.dhProgress = 25;
+
+                this.dhStatusText = '语音合成完成,正在识别数字人主体...';
+
+                this.refreshView();
+
+                this.dhGenerateWithAudio(imageUrl, res.data.audioUrl);
+
+              } else {
+
+                this.dhHandleError(res?.error?.message || '语音合成失败,请检查音色ID和文本');
+
+              }
+
+            },
+
+            error: (err) => {
+
+              this.dhSynthesizing = false;
+
+              this.dhHandleError(this.getErrorText(err, '语音合成请求失败'));
+
+            }
+
+          });
+
+        } else {
+
+          // 上传音频文件
+
+          this.dhStatusText = '形象图上传完成,正在上传音频文件...';
+
+          this.dhUploadingAudio = true;
+
+          this.refreshView();
+
+
+
+          const audioFile = this.dhUploadAudioFile!;
+
+          this.jimengService.uploadFileToParse(
+
+            audioFile,
+
+            `dh-audio-${Date.now()}-${audioFile.name}`,
+
+            audioFile.type || 'audio/mpeg'
+
+          ).subscribe({
+
+            next: (audioUrl: string) => {
+
+              this.dhUploadingAudio = false;
+
+              this.dhUploadAudioUrl = audioUrl;
+
+              this.dhProgress = 25;
+
+              this.dhStatusText = '音频上传完成,正在识别数字人主体...';
+
+              this.refreshView();
+
+              this.dhGenerateWithAudio(imageUrl, audioUrl);
+
+            },
+
+            error: (err) => {
+
+              this.dhUploadingAudio = false;
+
+              this.dhHandleError(err?.message || '上传音频文件失败');
+
+            }
+
+          });
+
+        }
+
+      },
+
+      error: (err) => {
+
+        this.dhHandleError(err?.message || '上传形象图失败');
+
+      }
+
+    });
+
+  }
+
+
+
+  private dhGenerateWithAudio(imageUrl: string, audioUrl: string): void {
+
+    console.log('🤖 数字人生成 - imageUrl:', imageUrl, 'audioUrl:', audioUrl);
+
+    this.jimengService.identifyDigitalHuman(imageUrl).subscribe({
+
+      next: ({ workId }) => {
+
+        this.jimengService.pollDigitalHumanIdentifyUntilReady(workId, (status, progress) => {
+
+          this.dhProgress = Math.min(25 + progress * 0.25, 50);
+
+          this.dhStatusText = status;
+
+          this.refreshView();
+
+        }).subscribe({
+
+          next: () => {
+
+            this.dhStatusText = '主体识别完成,开始生成数字人视频...';
+
+            this.dhProgress = 50;
+
+            this.refreshView();
+
+
+
+            this.jimengService.generateDigitalHuman(workId, audioUrl, {
+
+              prompt: this.dhPrompt,
+
+              peFastMode: this.dhResolution === '720p' ? true : this.dhFastMode,
+
+              outputResolution: this.dhResolution,
+
+              maskUrl: []
+
+            }, (status, progress) => {
+
+              this.dhProgress = Math.min(50 + progress * 0.45, 95);
+
+              this.dhStatusText = status;
+
+              this.refreshView();
+
+            }).subscribe({
+
+              next: (result) => {
+
+                this.dhProgress = 100;
+
+                this.dhResultVideoUrl = result.videoUrl;
+
+                this.dhStatusText = '数字人视频生成完成!';
+
+                this.dhLoading = false;
+
+                this.dhStep = 'done';
+
+                this.showToast('✅ 数字人视频已生成', 'success', 6000);
+
+
+
+                // 保存到结果
+
+                this.saveResult({
+
+                  type: 'video',
+
+                  url: result.videoUrl,
+
+                  title: `数字人合成 - ${new Date().toLocaleString('zh-CN')}`,
+
+                  quality: this.dhResolution === '1080p' ? '1080P' : '720P',
+
+                  duration: '数字人视频',
+
+                  cost: 0,
+
+                  segments: [{ id: 'DH-01', videoUrl: result.videoUrl, prompt: this.dhPrompt, status: 'completed' }]
+
+                });
+
+
+
+                this.saveHistoryRecord({
+
+                  keyword: '数字人合成',
+
+                  status: 'completed',
+
+                  cost: 0,
+
+                  duration: '数字人视频',
+
+                  styleName: '数字人合成',
+
+                  resultUrl: result.videoUrl,
+
+                  quality: this.dhResolution === '1080p' ? '1080P' : '720P',
+
+                  segmentCount: 1,
+
+                  successCount: 1
+
+                });
+
+
+
+                // 同步到视频管理
+
+                this.addDhVideoToManifest(result.videoUrl);
+
+
+
+                this.refreshView();
+
+              },
+
+              error: (err) => this.dhHandleError(typeof err === 'string' ? err : err?.message || err?.error?.message || '数字人视频生成失败')
+
+            });
+
+          },
+
+          error: (err) => this.dhHandleError(typeof err === 'string' ? err : err?.message || err?.error?.message || '数字人主体识别失败')
+
+        });
+
+      },
+
+      error: (err) => this.dhHandleError(typeof err === 'string' ? err : err?.message || err?.error?.message || '提交数字人主体识别失败')
+
+    });
+
+  }
+
+
+
+  private dhHandleError(message: string): void {
+
+    this.dhLoading = false;
+
+    this.dhSynthesizing = false;
+
+    this.dhUploadingAudio = false;
+
+    this.dhProgress = 0;
+
+    this.dhStep = 'done';
+
+    this.dhStatusText = message;
+
+    this.dhError = message;
+
+    this.showToast(`❌ ${message}`, 'error');
+
+    this.refreshView();
+
+  }
+
+
+
+  // ==================== 视频生成工作流 ====================
+
+
+
+  vgReset(): void {
+
+    this.vgStep = 1;
+
+    this.vgSourceFile = null;
+
+    this.vgSourceFileName = '';
+
+    this.vgSourceVideoUrl = '';
+
+    this.vgUploading = false;
+
+    this.vgUploadProgress = 0;
+
+    this.vgVideoId = '';
+
+    this.vgAnalyzing = false;
+
+    this.vgAnalysisText = '';
+
+    this.vgTranscript = '';
+
+    this.vgGeneratingScript = false;
+
+    this.vgScript = '';
+
+    this.vgScriptSegments = [];
+
+    this.vgGeneratingImages = false;
+
+    this.vgImageResults = [];
+
+    this.vgImageProgress = 0;
+
+    this.vgVoiceMode = 'system';
+
+    this.vgVoiceTimbreId = '';
+
+    this.vgSynthesizing = false;
+
+    this.vgAudioUrls = [];
+
+    this.vgCompositing = false;
+
+    this.vgCompositingProgress = 0;
+
+    this.vgFinalVideoUrl = '';
+
+    this.vgError = '';
+
+    this.vgTaskId = '';
+
+  }
+
+
+
+  // 视频生成:创建任务(上传成功后调用)
+
+  private vgCreateTask(): void {
+
+    const tempId = `VG-${Date.now()}`;
+
+    const task: Task = {
+
+      id: tempId,
+
+      keyword: this.vgSourceFileName || '视频生成',
+
+      status: 'processing',
+
+      step: 'vg-upload',
+
+      progress: 10,
+
+      cost: 0,
+
+      created_at: new Date(),
+
+      updated_at: new Date(),
+
+      original_video_title: this.vgSourceFileName,
+
+      type: 'video-generation',
+
+      vgData: this.vgBuildSnapshot()
+
+    };
+
+    this.vgTaskId = tempId;
+
+    this.activeTasks.unshift(task);
+
+    // saveTask POST 后端会返回真实 id,回填后同步 vgTaskId
+
+    this.taskSaveReady = new Promise<void>((resolve) => {
+
+      this.http.post<any>('/backend/api/tasks', task).subscribe({
+
+        next: (res) => {
+
+          if (res?.task?.id) {
+
+            task.id = res.task.id;
+
+            this.vgTaskId = res.task.id;
+
+          }
+
+          this.updateTaskStats();
+
+          console.log('💾 视频生成任务已保存:', task.id);
+
+          resolve();
+
+        },
+
+        error: (err) => {
+
+          console.warn('⚠️ 任务保存失败:', err);
+
+          resolve();
+
+        }
+
+      });
+
+    });
+
+    this.updateTaskStats();
+
+    this.showToast('任务已创建,可在「任务管理」中查看和恢复进度', 'success');
+
+  }
+
+
+
+  // 视频生成:更新任务进度(每个步骤转换时调用)
+
+  private vgUpdateTaskProgress(step: Task['step'], progress: number, status: Task['status'] = 'processing'): void {
+
+    const task = this.activeTasks.find(t => t.id === this.vgTaskId || (t.type === 'video-generation' && t.vgData?.videoId === this.vgVideoId));
+
+    if (!task) return;
+
+    // 若任务已被用户取消,则不再覆盖回 processing 状态
+
+    if (task.id && this.canceledTaskIds.has(task.id)) {
+
+      return;
+
+    }
+
+    this.vgTaskId = task.id;
+
+    task.step = step;
+
+    task.progress = progress;
+
+    task.status = status;
+
+    task.updated_at = new Date();
+
+    task.error_message = status === 'failed' ? this.vgError : undefined;
+
+    task.vgData = this.vgBuildSnapshot();
+
+    if (status === 'completed' && this.vgFinalVideoUrl) {
+
+      task.result_url = this.vgFinalVideoUrl;
+
+    }
+
+    this.updateTask(task);
+
+    this.updateTaskStats();
+
+  }
+
+
+
+  // 视频生成:构建当前状态快照
+
+  private vgBuildSnapshot(): Task['vgData'] {
+
+    return {
+
+      videoId: this.vgVideoId,
+
+      sourceFileName: this.vgSourceFileName,
+
+      sourceVideoUrl: this.vgSourceVideoUrl,
+
+      step: this.vgStep,
+
+      transcript: this.vgTranscript,
+
+      analysisText: this.vgAnalysisText,
+
+      script: this.vgScript,
+
+      scriptSegments: this.vgScriptSegments,
+
+      imageResults: this.vgImageResults,
+
+      audioUrls: this.vgAudioUrls,
+
+      voiceMode: this.vgVoiceMode,
+
+      voiceSpeakerId: this.vgVoiceSpeakerId,
+
+      voiceTimbreId: this.vgVoiceTimbreId,
+
+      finalVideoUrl: this.vgFinalVideoUrl
+
+    };
+
+  }
+
+
+
+  // 视频生成:从任务恢复状态
+
+  vgResumeFromTask(task: Task): void {
+
+    if (!task.vgData) {
+
+      this.showToast('该任务没有可恢复的数据', 'warn');
+
+      return;
+
+    }
+
+    const d = task.vgData;
+
+    this.vgTaskId = task.id;
+
+    this.vgVideoId = d.videoId;
+
+    this.vgSourceFileName = d.sourceFileName;
+
+    this.vgSourceVideoUrl = d.sourceVideoUrl;
+
+    this.vgSourceFile = null; // File 对象无法持久化
+
+    this.vgStep = d.step;
+
+    this.vgTranscript = d.transcript;
+
+    this.vgAnalysisText = d.analysisText;
+
+    this.vgScript = d.script;
+
+    this.vgScriptSegments = d.scriptSegments || [];
+
+    this.vgImageResults = d.imageResults || [];
+
+    this.vgAudioUrls = d.audioUrls || [];
+
+    this.vgVoiceMode = d.voiceMode || 'system';
+
+    this.vgVoiceSpeakerId = d.voiceSpeakerId || 'zh_female_shuangkuai-am_16k';
+
+    this.vgVoiceTimbreId = d.voiceTimbreId || '';
+
+    this.vgFinalVideoUrl = d.finalVideoUrl || '';
+
+    this.vgError = '';
+
+    this.vgUploading = false;
+
+    this.vgAnalyzing = false;
+
+    this.vgGeneratingScript = false;
+
+    this.vgGeneratingImages = false;
+
+    this.vgSynthesizing = false;
+
+    this.vgCompositing = false;
+
+    // 切换到视频生成页
+
+    this.setCurrentTab('video-generation');
+
+    this.showToast(`已恢复任务:${d.sourceFileName},当前在第${d.step}步`, 'success');
+
+    this.refreshView();
+
+  }
+
+
+
+  // Step 1: 选择源视频文件
+
+  vgSelectFile(event: Event): void {
+
+    const input = event.target as HTMLInputElement;
+
+    const file = input?.files?.[0];
+
+    if (!file) return;
+
+    if (!file.type.startsWith('video/')) {
+
+      this.showToast('请选择视频文件', 'warn');
+
+      return;
+
+    }
+
+    this.vgSourceFile = file;
+
+    this.vgSourceFileName = file.name;
+
+    this.vgSourceVideoUrl = URL.createObjectURL(file);
+
+    this.vgError = '';
+
+  }
+
+
+
+  // Step 1 → 2: 上传视频并进入解析
+
+  vgUploadAndAnalyze(): void {
+
+    if (!this.vgSourceFile || this.vgUploading) return;
+
+    this.vgUploading = true;
+
+    this.vgUploadProgress = 0;
+
+    this.vgError = '';
+
+
+
+    const formData = new FormData();
+
+    formData.append('video', this.vgSourceFile);
+
+    formData.append('title', `视频生成源 - ${new Date().toLocaleString('zh-CN')}`);
+
+
+
+    const xhr = new XMLHttpRequest();
+
+    xhr.open('POST', '/backend/api/upload/video');
+
+
+
+    xhr.upload.onprogress = (e) => {
+
+      if (e.lengthComputable) {
+
+        this.vgUploadProgress = Math.round((e.loaded / e.total) * 100);
+
+        this.cdr.detectChanges();
+
+      }
+
+    };
+
+
+
+    xhr.onload = () => {
+
+      this.ngZone.run(() => {
+
+        this.vgUploading = false;
+
+        if (xhr.status === 200) {
+
+          try {
+
+            const res = JSON.parse(xhr.responseText);
+
+            if (res?.success && res?.video?.id) {
+
+              this.vgVideoId = res.video.id;
+
+              this.vgStep = 2;
+
+              this.vgCreateTask();
+
+              this.vgUpdateTaskProgress('vg-analyze', 15);
+
+              this.vgStartAnalysis();
+
+            } else {
+
+              this.vgError = '上传返回异常';
+
+            }
+
+          } catch { this.vgError = '解析上传结果失败'; }
+
+        } else {
+
+          this.vgError = `上传失败 (${xhr.status})`;
+
+        }
+
+        this.cdr.detectChanges();
+
+      });
+
+    };
+
+
+
+    xhr.onerror = () => {
+
+      this.ngZone.run(() => {
+
+        this.vgUploading = false;
+
+        this.vgError = '网络错误,上传失败';
+
+        this.cdr.detectChanges();
+
+      });
+
+    };
+
+
+
+    xhr.send(formData);
+
+  }
+
+
+
+  // Step 2: Gemini 视频识别(分析 + 转录),Whisper 作为降级方案
+
+  private vgStartAnalysis(): void {
+
+    this.vgAnalyzing = true;
+
+    this.vgTranscript = '';
+
+    this.vgAnalysisText = '';
+
+    this.refreshView();
+
+
+
+    const fileSizeMB = this.vgSourceFile ? this.vgSourceFile.size / 1024 / 1024 : 0;
+
+
+
+    if (this.vgSourceFile && fileSizeMB <= 20) {
+
+      // ≤20MB:Gemini 直接分析视频(画面+音频+转录)
+
+      this.vgAnalysisText = `正在将视频 (${fileSizeMB.toFixed(1)}MB) 发送给 Gemini 进行视频理解...`;
+
+      this.refreshView();
+
+      this.vgRunGeminiAnalysisAndTranscript();
+
+    } else if (this.vgVideoId) {
+
+      // >20MB:后端 ffmpeg 提取音轨 → Gemini 音频识别 + 转录
+
+      this.vgAnalysisText = `视频 ${fileSizeMB.toFixed(1)}MB 超过 Gemini 视频限制,正在提取音轨用 Gemini 音频识别...`;
+
+      this.refreshView();
+
+      this.vgRunAudioExtractAndGemini();
+
+    } else {
+
+      // 无文件 → Whisper 降级
+
+      this.vgAnalysisText = '使用 Whisper 语音识别...';
+
+      this.refreshView();
+
+      this.vgRunWhisperFallback();
+
+    }
+
+  }
+
+
+
+  // Gemini 一次请求:视频分析 + 精确转录
+
+  private async vgRunGeminiAnalysisAndTranscript(): Promise<void> {
+
+    try {
+
+      const base64 = await this.llmService.fileToBase64(this.vgSourceFile!);
+
+      const mimeType = this.vgSourceFile!.type || 'video/mp4';
+
+
+
+      const prompt = `你是一个专业的视频内容分析师。请仔细观看这个视频,完成以下两个任务:
+
+
+
+===== 任务一:视频内容分析 =====
+
+从画面和音频两个维度进行全面分析,提取:
+
+1. **视频主题和核心观点**:视频讲了什么
+
+2. **画面内容描述**:出现了哪些场景、人物、物体、文字
+
+3. **目标受众**:适合什么人群观看
+
+4. **内容风格**:教程/评测/新闻/故事/口播/Vlog等
+
+5. **关键知识点或亮点**(3-5条)
+
+6. **视觉风格评价**:画面质量、构图、色调、字幕等
+
+7. **建议的改进方向**
+
+
+
+===== 任务二:完整语音转录 =====
+
+请将视频中所有语音内容逐字转录为中文文本。要求:
+
+- 忠实还原每一句话,不遗漏、不改写、不总结
+
+- 保留说话人的原始用词和语气
+
+- 按自然段落分段
+
+- 如有专业术语或人名,请尽量准确
+
+
+
+请严格按以下格式输出(用分隔线区分两部分):
+
+
+
+【内容分析】
+
+(这里输出任务一的分析结果)
+
+
+
+---TRANSCRIPT_START---
+
+(这里输出任务二的完整语音转录文本,只包含转录内容,不要加任何说明)
+
+---TRANSCRIPT_END---`;
+
+
+
+      this.llmService.analyzeVideo(base64, mimeType, prompt, {
+
+        model: 'gemini-2.5-flash',
+
+        generationConfig: { maxOutputTokens: 8192 }
+
+      }).subscribe({
+
+        next: (result) => {
+
+          this.vgParseGeminiResult(result);
+
+          this.vgAnalyzing = false;
+
+          this.vgUpdateTaskProgress('vg-analyze', 25);
+
+          this.refreshView();
+
+        },
+
+        error: (err) => {
+
+          console.warn('⚠️ Gemini 视频识别失败,降级为 Whisper:', err);
+
+          this.vgAnalysisText = 'Gemini 识别失败,正在使用 Whisper 语音识别...';
+
+          this.refreshView();
+
+          this.vgRunWhisperFallback();
+
+        }
+
+      });
+
+    } catch (err) {
+
+      console.warn('⚠️ 视频文件读取失败:', err);
+
+      this.vgAnalysisText = '视频文件读取失败,正在使用 Whisper 语音识别...';
+
+      this.refreshView();
+
+      this.vgRunWhisperFallback();
+
+    }
+
+  }
+
+
+
+  // 解析 Gemini 返回结果:拆分「内容分析」和「转录文本」
+
+  private vgParseGeminiResult(raw: string): void {
+
+    const transcriptMatch = raw.match(/---TRANSCRIPT_START---([\s\S]*?)---TRANSCRIPT_END---/);
+
+    if (transcriptMatch) {
+
+      this.vgTranscript = transcriptMatch[1].trim();
+
+      // 分析部分 = 转录标记之前的内容
+
+      const analysisPart = raw.substring(0, raw.indexOf('---TRANSCRIPT_START---')).trim();
+
+      this.vgAnalysisText = analysisPart.replace(/^【内容分析】\s*/, '') || '分析完成';
+
+      console.log(`✅ Gemini 分析+转录完成 (转录 ${this.vgTranscript.length} 字)`);
+
+    } else {
+
+      // 没有找到转录标记,整体作为分析结果
+
+      this.vgAnalysisText = raw || '分析完成';
+
+      console.warn('⚠️ Gemini 未返回转录标记,需手动输入文字稿');
+
+    }
+
+  }
+
+
+
+  // >20MB 视频:后端 ffmpeg 提取音轨 → Gemini 音频识别(分析+转录)
+
+  private vgRunAudioExtractAndGemini(): void {
+
+    this.http.post<any>('/backend/api/extract-audio-mp3', {
+
+      videoId: this.vgVideoId
+
+    }).subscribe({
+
+      next: (res) => {
+
+        if (res?.success && res?.audio?.base64) {
+
+          const audioSizeMB = res.audio.sizeMB || 0;
+
+          console.log(`🎵 音轨提取完成 (${audioSizeMB}MB),发送给 Gemini 音频识别...`);
+
+          this.vgAnalysisText = `音轨提取完成 (${audioSizeMB.toFixed(1)}MB),正在 Gemini 音频识别...`;
+
+          this.refreshView();
+
+
+
+          if (audioSizeMB > 20) {
+
+            // 音频仍然过大,降级为 Whisper
+
+            console.warn('⚠️ 提取的音频仍超过 20MB,降级为 Whisper');
+
+            this.vgAnalysisText = '音频文件仍较大,改用 Whisper 识别...';
+
+            this.refreshView();
+
+            this.vgRunWhisperFallback();
+
+            return;
+
+          }
+
+
+
+          const prompt = `你是一个专业的音频内容分析师。请仔细听这段音频,完成以下两个任务:
+
+
+
+===== 任务一:内容分析 =====
+
+从音频内容进行分析,提取:
+
+1. **主题和核心观点**:音频讲了什么
+
+2. **目标受众**:适合什么人群
+
+3. **内容风格**:教程/评测/新闻/故事/口播等
+
+4. **关键知识点或亮点**(3-5条)
+
+5. **建议的改进方向**
+
+
+
+===== 任务二:完整语音转录 =====
+
+请将音频中所有语音内容逐字转录为中文文本。要求:
+
+- 忠实还原每一句话,不遗漏、不改写、不总结
+
+- 保留说话人的原始用词和语气
+
+- 按自然段落分段
+
+- 如有专业术语或人名,请尽量准确
+
+
+
+请严格按以下格式输出(用分隔线区分两部分):
+
+
+
+【内容分析】
+
+(这里输出任务一的分析结果)
+
+
+
+---TRANSCRIPT_START---
+
+(这里输出任务二的完整语音转录文本,只包含转录内容,不要加任何说明)
+
+---TRANSCRIPT_END---`;
+
+
+
+          this.llmService.analyzeAudio(res.audio.base64, res.audio.mimeType || 'audio/mp3', prompt, {
+
+            model: 'gemini-2.5-flash',
+
+            generationConfig: { maxOutputTokens: 8192 }
+
+          }).subscribe({
+
+            next: (result) => {
+
+              this.vgParseGeminiResult(result);
+
+              this.vgAnalyzing = false;
+
+              this.refreshView();
+
+            },
+
+            error: (err) => {
+
+              console.warn('⚠️ Gemini 音频识别失败,降级为 Whisper:', err);
+
+              this.vgAnalysisText = 'Gemini 音频识别失败,改用 Whisper...';
+
+              this.refreshView();
+
+              this.vgRunWhisperFallback();
+
+            }
+
+          });
+
+        } else {
+
+          console.warn('⚠️ 音轨提取未返回数据,降级为 Whisper');
+
+          this.vgRunWhisperFallback();
+
+        }
+
+      },
+
+      error: (err) => {
+
+        const detail = err?.error?.error || err?.message || `HTTP ${err?.status}`;
+
+        console.warn('⚠️ 音轨提取失败:', detail, err);
+
+        this.vgAnalysisText = `音轨提取失败 (${detail}),改用 Whisper...`;
+
+        this.refreshView();
+
+        this.vgRunWhisperFallback();
+
+      }
+
+    });
+
+  }
+
+
+
+  // 降级方案:Whisper 转录 + GPT 文本分析
+
+  private vgRunWhisperFallback(): void {
+
+    this.http.post<any>('/backend/api/whisper/transcribe', {
+
+      videoId: this.vgVideoId,
+
+      language: 'Chinese',
+
+      model: 'large'
+
+    }).subscribe({
+
+      next: (res) => {
+
+        if (res?.success && res?.transcript) {
+
+          this.vgTranscript = res.transcript;
+
+          this.vgAnalysisText = 'Whisper 转录完成,正在 AI 分析内容...';
+
+          this.refreshView();
+
+          // 用 GPT 做文本分析
+
+          this.vgDoTextAnalysis();
+
+        } else {
+
+          this.vgAnalyzing = false;
+
+          this.vgAnalysisText = '语音识别完成但未返回文字稿,请手动输入';
+
+          this.refreshView();
+
+        }
+
+      },
+
+      error: (err) => {
+
+        this.vgAnalyzing = false;
+
+        this.vgAnalysisText = `Whisper 识别失败: ${err?.error?.error || err?.message || '未知错误'}。请手动输入文字稿。`;
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  private vgDoTextAnalysis(): void {
+
+    const systemPrompt = `你是一个专业的视频内容分析师。请分析以下视频的转录文字稿,提取:
+
+1. 视频主题和核心观点
+
+2. 目标受众
+
+3. 内容风格(教程/评测/新闻/故事等)
+
+4. 关键知识点或亮点(3-5条)
+
+5. 建议的改进方向
+
+
+
+请用简洁的中文回复。`;
+
+
+
+    this.llmService.askWithSystem(systemPrompt, this.vgTranscript, { model: 'gemini-2.5-flash', max_tokens: 1024 }).subscribe({
+
+      next: (analysis) => {
+
+        this.vgAnalysisText = analysis || '分析完成';
+
+        this.vgAnalyzing = false;
+
+        this.refreshView();
+
+      },
+
+      error: () => {
+
+        this.vgAnalysisText = '(AI 文本分析暂不可用,已完成 Whisper 转录)';
+
+        this.vgAnalyzing = false;
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  vgConfirmAnalysis(): void {
+
+    if (!this.vgTranscript.trim()) {
+
+      this.showToast('请先完成视频转录或手动输入文字稿', 'warn');
+
+      return;
+
+    }
+
+    this.vgStep = 3;
+
+    this.vgUpdateTaskProgress('vg-script', 30);
+
+    this.vgGenerateScript();
+
+  }
+
+
+
+  // Step 3: LLM 生成分镜脚本
+
+  private vgGenerateScript(): void {
+
+    this.vgGeneratingScript = true;
+
+    this.vgScript = '';
+
+    this.vgScriptSegments = [];
+
+
+
+    const systemPrompt = `你是一个专业的短视频脚本编剧。根据以下视频转录文字稿,生成一个完整的分镜脚本。
+
+
+
+⚠️ 最重要的要求:你必须完整覆盖原文的全部内容,不得遗漏任何段落或要点!将原文从头到尾全部改写为分镜旁白,确保信息量100%覆盖。
+
+
+
+要求:
+
+1. 根据内容长度自动决定片段数量(短文5-8段,长文8-15段),确保原文每一段内容都被覆盖到
+
+2. 每个片段包含:旁白文本(narration)、配图英文提示词(imagePrompt)、配图中文提示词(imagePromptCn)
+
+3. 旁白文本要求:必须包含原文中的所有关键信息、数据、观点,不能省略或概括,只做口语化改写
+
+4. 配图中文提示词(imagePromptCn)是最重要的配图生成依据,要求:
+
+   - 描述一幅完整的16:9横版信息图/示意图画面
+
+   - 必须全部用中文描述,图片中的所有文字标注都是中文
+
+   - 内容紧密关联该段旁白,提取关键数据、概念、流程等
+
+   - 风格:白色或浅色简洁背景,扁平化设计,信息图表/流程图/概念图
+
+   - 包含中文大标题、关键词、数据可视化、图标、箭头等元素
+
+   - 注意:提示词中不要出现"PPT"、"幻灯片"、"演示文稿"等字样,只描述画面内容本身
+
+5. 配图英文提示词(imagePrompt)是备选提示词,描述同样的画面内容
+
+
+
+请严格按以下 JSON 格式返回(不要包含其他文字,不要用 markdown 代码块包裹):
+
+[
+
+  {"id": "S-01", "narration": "旁白文本...", "imagePrompt": "English: 16:9 infographic...", "imagePromptCn": "16:9横版信息图,白色背景,大标题"xxx",下方用流程图展示xxx的步骤..."},
+
+  {"id": "S-02", "narration": "旁白文本...", "imagePrompt": "English: 16:9 infographic...", "imagePromptCn": "16:9横版信息图,浅色背景,标题"xxx",用图标和箭头展示xxx的关系..."}
+
+]`;
+
+
+
+    this.llmService.askWithSystem(systemPrompt, this.vgTranscript, { model: 'gemini-2.5-flash', max_tokens: 16384 }).subscribe({
+
+      next: (result) => {
+
+        this.vgScript = result;
+
+        this.vgGeneratingScript = false;
+
+        this.vgParseScriptSegments(result);
+
+        this.vgUpdateTaskProgress('vg-script', 40);
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        this.vgGeneratingScript = false;
+
+        this.vgError = `脚本生成失败: ${err?.message || '请重试'}`;
+
+        this.vgUpdateTaskProgress('vg-script', 30, 'failed');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  private vgParseScriptSegments(raw: string): void {
+
+    console.log('📝 原始脚本响应:', raw.substring(0, 500));
+
+
+
+    // 1. 去除 markdown code block 包裹
+
+    let cleaned = raw;
+
+    const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/);
+
+    if (codeBlockMatch) {
+
+      cleaned = codeBlockMatch[1].trim();
+
+    }
+
+
+
+    // 2. 尝试提取 JSON 数组
+
+    const jsonMatch = cleaned.match(/\[[\s\S]*\]/);
+
+    if (jsonMatch) {
+
+      cleaned = jsonMatch[0];
+
+    }
+
+
+
+    // 3. 修复常见 JSON 问题(尾部逗号)
+
+    cleaned = cleaned.replace(/,\s*([}\]])/g, '$1');
+
+
+
+    try {
+
+      const parsed = JSON.parse(cleaned);
+
+      if (Array.isArray(parsed) && parsed.length > 0) {
+
+        this.vgScriptSegments = parsed.map((item: any, i: number) => ({
+
+          id: item.id || `S-${String(i + 1).padStart(2, '0')}`,
+
+          narration: item.narration || '',
+
+          imagePrompt: item.imagePrompt || item.image_prompt || '',
+
+          imagePromptCn: item.imagePromptCn || item.image_prompt_cn || ''
+
+        }));
+
+        console.log(`✅ 脚本解析成功: ${this.vgScriptSegments.length} 个片段`);
+
+        return;
+
+      }
+
+    } catch (e) {
+
+      console.warn('⚠️ JSON.parse 失败:', (e as Error).message);
+
+    }
+
+
+
+    // 4. 逐对象提取 fallback:正则匹配每个 {...} 块
+
+    try {
+
+      const objMatches = cleaned.match(/\{[^{}]*\}/g);
+
+      if (objMatches && objMatches.length > 0) {
+
+        const segments: any[] = [];
+
+        for (const objStr of objMatches) {
+
+          try {
+
+            const obj = JSON.parse(objStr.replace(/,\s*}/g, '}'));
+
+            if (obj.narration) segments.push(obj);
+
+          } catch {}
+
+        }
+
+        if (segments.length > 0) {
+
+          this.vgScriptSegments = segments.map((item: any, i: number) => ({
+
+            id: item.id || `S-${String(i + 1).padStart(2, '0')}`,
+
+            narration: item.narration || '',
+
+            imagePrompt: item.imagePrompt || item.image_prompt || '',
+
+            imagePromptCn: item.imagePromptCn || item.image_prompt_cn || ''
+
+          }));
+
+          console.log(`✅ 逐对象解析成功: ${this.vgScriptSegments.length} 个片段`);
+
+          return;
+
+        }
+
+      }
+
+    } catch {}
+
+
+
+    // 5. 最终 fallback:保留原始文本供用户手动编辑
+
+    console.warn('⚠️ 脚本解析全部失败,显示原文供手动编辑');
+
+    this.vgScriptSegments = [{
+
+      id: 'S-01',
+
+      narration: raw.replace(/```[\s\S]*?```/g, '').trim(),
+
+      imagePrompt: 'Professional PPT slide illustration, clean white background, infographic style',
+
+      imagePromptCn: '(脚本自动解析失败,请手动拆分片段)'
+
+    }];
+
+  }
+
+
+
+  vgConfirmScript(): void {
+
+    if (this.vgScriptSegments.length === 0) {
+
+      this.showToast('请先生成或编辑脚本', 'warn');
+
+      return;
+
+    }
+
+    this.vgStep = 4;
+
+    this.vgUpdateTaskProgress('vg-image', 45);
+
+    // 初始化图片结果列表
+
+    this.vgImageResults = this.vgScriptSegments.map(seg => ({
+
+      id: seg.id,
+
+      prompt: seg.imagePromptCn || seg.imagePrompt,
+
+      imageUrl: '',
+
+      status: 'pending' as const
+
+    }));
+
+  }
+
+
+
+  // Step 4: 批量生成配图(使用即梦 API)
+
+  vgGenerateImages(): void {
+
+    if (this.vgGeneratingImages) return;
+
+    this.vgGeneratingImages = true;
+
+    this.vgImageProgress = 0;
+
+
+
+    const pending = this.vgImageResults.filter(r => r.status === 'pending');
+
+    if (pending.length === 0) {
+
+      this.vgGeneratingImages = false;
+
+      return;
+
+    }
+
+
+
+    let completed = 0;
+
+    const total = pending.length;
+
+
+
+    const RETRY_MAX = 5;
+
+    const RETRY_DELAY = 15000; // 并发超限时等 15 秒重试
+
+    const GAP_DELAY = 6000;   // 每张图之间间隔 6 秒,避免并发
+
+
+
+    const generateNext = (index: number, retryCount: number = 0) => {
+
+      if (index >= pending.length) {
+
+        this.vgGeneratingImages = false;
+
+        this.vgUpdateTaskProgress('vg-image', 60);
+
+        this.showToast(`配图生成完成 (${completed}/${total})`, completed > 0 ? 'success' : 'warn');
+
+        this.refreshView();
+
+        return;
+
+      }
+
+
+
+      const item = pending[index];
+
+      item.status = 'generating';
+
+      this.refreshView();
+
+
+
+      this.jimengService.generateImage(item.prompt, 1920, 1080).subscribe({
+
+        next: (res: any) => {
+
+          const workId = res?.data?.workId || res?.workId;
+
+          if (!workId) {
+
+            item.status = 'failed';
+
+            completed++;
+
+            this.refreshView();
+
+            setTimeout(() => generateNext(index + 1), GAP_DELAY);
+
+            return;
+
+          }
+
+          this.jimengService.pollUntilComplete(workId, 'getText2ImgV31', (status: any, attempt: number) => {
+
+            this.vgImageProgress = Math.round(((completed + Math.min(attempt / 20, 0.9)) / total) * 100);
+
+            this.refreshView();
+
+          }).subscribe({
+
+            next: (result: any) => {
+
+              const imageUrl = result?.images?.[0] || result?.url || '';
+
+              item.imageUrl = imageUrl;
+
+              item.status = imageUrl ? 'done' : 'failed';
+
+              completed++;
+
+              this.vgImageProgress = Math.round((completed / total) * 100);
+
+              this.refreshView();
+
+              setTimeout(() => generateNext(index + 1), GAP_DELAY);
+
+            },
+
+            error: () => {
+
+              item.status = 'failed';
+
+              completed++;
+
+              this.refreshView();
+
+              setTimeout(() => generateNext(index + 1), GAP_DELAY);
+
+            }
+
+          });
+
+        },
+
+        error: (err) => {
+
+          const errMsg = err?.error?.error?.message || err?.error?.message || err?.message || '';
+
+          const isRetryable = errMsg.includes('并发超限') || err?.status === 500 || err?.status === 429;
+
+          if (isRetryable && retryCount < RETRY_MAX) {
+
+            const backoff = RETRY_DELAY * (retryCount + 1); // 递增退避: 15s, 30s, 45s...
+
+            console.warn(`⚠️ 配图 ${item.id} 请求失败(${errMsg || err?.status}),${backoff / 1000}s 后第${retryCount + 1}次重试...`);
+
+            item.status = 'pending';
+
+            this.refreshView();
+
+            setTimeout(() => generateNext(index, retryCount + 1), backoff);
+
+          } else {
+
+            console.error(`❌ 配图 ${item.id} 最终失败:`, errMsg || err?.status);
+
+            item.status = 'failed';
+
+            completed++;
+
+            this.refreshView();
+
+            setTimeout(() => generateNext(index + 1), GAP_DELAY);
+
+          }
+
+        }
+
+      });
+
+    };
+
+
+
+    generateNext(0);
+
+  }
+
+
+
+  vgConfirmImages(): void {
+
+    this.vgStep = 5;
+
+    this.vgUpdateTaskProgress('vg-voice', 65);
+
+  }
+
+
+
+  // Step 5: 语音配音
+
+  vgSynthesizeAll(): void {
+
+    if (this.vgSynthesizing) return;
+
+    this.vgSynthesizing = true;
+
+    this.vgAudioUrls = [];
+
+    this.vgError = '';
+
+
+
+    const segments = this.vgScriptSegments;
+
+    let index = 0;
+
+
+
+    const synthNext = () => {
+
+      if (index >= segments.length) {
+
+        this.vgSynthesizing = false;
+
+        this.vgUpdateTaskProgress('vg-voice', 80);
+
+        this.showToast(`配音完成 (${this.vgAudioUrls.length}/${segments.length})`, 'success');
+
+        this.refreshView();
+
+        return;
+
+      }
+
+
+
+      const seg = segments[index];
+
+      const text = seg.narration;
+
+      if (!text.trim()) {
+
+        index++;
+
+        synthNext();
+
+        return;
+
+      }
+
+
+
+      // 使用远程 TTS 合成(server.fmode.cn)
+
+      const payload: any = {
+
+        token: this.voiceApiToken,
+
+        text,
+
+        isStream: false,
+
+        audio_params: {
+
+          format: 'mp3',
+
+          sample_rate: 24000,
+
+          speech_rate: 0,
+
+          loudness_rate: 0
+
+        }
+
+      };
+
+
+
+      if (this.vgVoiceMode === 'clone' && this.vgVoiceTimbreId) {
+
+        payload.timbreId = this.vgVoiceTimbreId;
+
+      } else {
+
+        payload.speaker_id = this.vgVoiceSpeakerId || 'zh_female_shuangkuai-am_16k';
+
+      }
+
+
+
+      this.http.post<any>(`${this.voiceTtsBaseUrl}/unidirectional`, payload).subscribe({
+
+        next: (res) => {
+
+          const audioUrl = res?.data?.audioUrl || '';
+
+          if (audioUrl) {
+
+            this.vgAudioUrls.push({ segId: seg.id, audioUrl });
+
+          } else {
+
+            console.warn(`配音片段 ${seg.id}: 未返回 audioUrl`, res);
+
+          }
+
+          index++;
+
+          this.refreshView();
+
+          synthNext();
+
+        },
+
+        error: (err) => {
+
+          console.warn(`配音片段 ${seg.id} 失败:`, err);
+
+          index++;
+
+          synthNext();
+
+        }
+
+      });
+
+    };
+
+
+
+    synthNext();
+
+  }
+
+
+
+  vgConfirmVoice(): void {
+
+    this.vgStep = 6;
+
+    this.vgUpdateTaskProgress('vg-composite', 85);
+
+  }
+
+
+
+  // Step 6: 合成最终视频(图片+音频 → 视频)
+
+  vgComposite(): void {
+
+    this.vgCompositing = true;
+
+    this.vgCompositingProgress = 0;
+
+    this.vgFinalVideoUrl = '';
+
+    this.vgError = '';
+
+    this.refreshView();
+
+
+
+    // 构建片段数据:匹配图片和音频
+
+    const segments: { id: string; imageUrl: string; audioUrl: string; }[] = [];
+
+    for (const seg of this.vgScriptSegments) {
+
+      const img = this.vgImageResults.find(r => r.id === seg.id);
+
+      const audio = this.vgAudioUrls.find(a => a.segId === seg.id);
+
+      if (img?.imageUrl && audio?.audioUrl) {
+
+        segments.push({ id: seg.id, imageUrl: img.imageUrl, audioUrl: audio.audioUrl });
+
+      }
+
+    }
+
+
+
+    if (segments.length === 0) {
+
+      this.vgCompositing = false;
+
+      this.vgError = '没有可用的图片+音频配对,无法合成视频';
+
+      this.showToast(this.vgError, 'error');
+
+      this.refreshView();
+
+      return;
+
+    }
+
+
+
+    // 模拟进度(因为后端是同步处理,用定时器模拟进度条)
+
+    let fakeProgress = 0;
+
+    const progressTimer = setInterval(() => {
+
+      fakeProgress = Math.min(fakeProgress + 2, 90);
+
+      this.vgCompositingProgress = fakeProgress;
+
+      this.refreshView();
+
+    }, 1000);
+
+
+
+    this.http.post<any>('/backend/api/video/composite', {
+
+      segments,
+
+      title: this.vgSourceFileName?.replace(/\.[^.]+$/, '') || '视频生成'
+
+    }).subscribe({
+
+      next: (res) => {
+
+        clearInterval(progressTimer);
+
+        this.vgCompositingProgress = 100;
+
+        this.vgCompositing = false;
+
+        if (res?.success && res?.videoUrl) {
+
+          this.vgFinalVideoUrl = '/backend' + res.videoUrl;
+
+          this.vgUpdateTaskProgress('vg-composite', 100, 'completed');
+
+          this.showToast(`视频合成完成!共 ${res.segments} 个片段`, 'success');
+
+        } else {
+
+          this.vgError = '合成返回结果异常';
+
+          this.vgUpdateTaskProgress('vg-composite', 90, 'failed');
+
+          this.showToast(this.vgError, 'error');
+
+        }
+
+        this.refreshView();
+
+      },
+
+      error: (err) => {
+
+        clearInterval(progressTimer);
+
+        this.vgCompositing = false;
+
+        this.vgCompositingProgress = 0;
+
+        this.vgError = `视频合成失败: ${err?.error?.error || err?.message || '请重试'}`;
+
+        this.vgUpdateTaskProgress('vg-composite', 85, 'failed');
+
+        this.showToast(this.vgError, 'error');
+
+        this.refreshView();
+
+      }
+
+    });
+
+  }
+
+
+
+  // 回到上一步
+
+  vgPrevStep(): void {
+
+    if (this.vgStep > 1) this.vgStep--;
+
+  }
+
+
+
+  // ===== 卡片瀑布展开/折叠控制 =====
+
+  vgGetStepState(n: number): 'done' | 'active' | 'locked' {
+
+    if (n < this.vgStep) return 'done';
+
+    if (n === this.vgStep) return 'active';
+
+    return 'locked';
+
+  }
+
+  vgIsStepExpanded(n: number): boolean {
+
+    if (n > this.vgStep) return false;       // 未解锁:永远折叠
+
+    if (n === this.vgStep) return true;      // 当前步骤:始终展开
+
+    return this.vgUserExpandedSteps.has(n);  // 已完成:仅在用户手动展开时显示
+
+  }
+
+  vgToggleStepExpand(n: number): void {
+
+    if (n >= this.vgStep) return;            // 当前/未解锁不可切换
+
+    if (this.vgUserExpandedSteps.has(n)) this.vgUserExpandedSteps.delete(n);
+
+    else this.vgUserExpandedSteps.add(n);
+
+  }
+
+  // 跳转到某个已完成步骤进行编辑(同时展开它)
+
+  vgEditStep(n: number): void {
+
+    if (n > this.vgStep) return;             // 不能跳到未解锁
+
+    if (n < this.vgStep) {
+
+      const downstream = this.vgStep - n;
+
+      const ok = confirm(`返回步骤 ${n}「${this.vgStepLabels[n - 1]}」编辑后,需要重新确认下游 ${downstream} 个步骤,相关结果(脚本/配图/配音/合成)可能会被覆盖。是否继续?`);
+
+      if (!ok) return;
+
+    }
+
+    this.vgStep = n;
+
+    // 同时把该步骤从用户展开集合中清除(active 步骤会自动展开)
+
+    this.vgUserExpandedSteps.delete(n);
+
+  }
+
+
+
+  // 重新生成脚本
+
+  vgRegenerateScript(): void {
+
+    this.vgGenerateScript();
+
+  }
+
+
+
+  private addDhVideoToManifest(videoUrl: string): void {
+
+    const now = new Date();
+
+    const id = `DH-${Date.now()}`;
+
+    const title = `数字人合成 - ${now.toLocaleString('zh-CN')}`;
+
+    const filename = videoUrl.split('/').pop() || `digital-human-${id}.mp4`;
+
+    const newEntry = {
+
+      id,
+
+      title,
+
+      filename,
+
+      filepath: videoUrl,
+
+      size: 0,
+
+      duration: 0,
+
+      created_at: now.toISOString(),
+
+      modified_at: now.toISOString(),
+
+      category: 'general',
+
+      tags: ['数字人', 'AI生成'],
+
+      description: `数字人合成视频 (${this.dhResolution})`,
+
+      source: 'generated',
+
+      metadata: {
+
+        resolution: this.dhResolution === '1080p' ? '1920x1080' : '1280x720',
+
+        format: 'mp4'
+
+      }
+
+    };
+
+
+
+    this.http.post<any>('/backend/api/manifest', newEntry).subscribe({
+
+      next: (res) => {
+
+        if (res?.success) {
+
+          this.managedVideos.unshift({
+
+            ...newEntry,
+
+            created_at: now,
+
+            modified_at: now,
+
+            source: 'generated' as const
+
+          } as ManagedVideo);
+
+          this.updateCategoryCounts();
+
+          console.log('📁 数字人视频已同步到视频管理:', id);
+
+        }
+
+      },
+
+      error: (err) => console.warn('⚠️ 同步视频管理失败:', err)
+
+    });
+
+  }
+
+
+
+}
+

+ 208 - 0
src/app/components/api-test/api-test.component.ts

@@ -0,0 +1,208 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { DouyinApiService } from '../../services/douyin-api.service';
+
+@Component({
+  selector: 'app-api-test',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  template: `
+    <div class="api-test-container">
+      <h2>🔧 API测试工具</h2>
+
+      <div class="test-section">
+        <h3>搜索测试</h3>
+        <div class="input-group">
+          <input
+            [(ngModel)]="searchKeyword"
+            placeholder="输入搜索关键词"
+            class="test-input"
+          />
+          <button
+            (click)="testSearch()"
+            [disabled]="isSearching"
+            class="test-btn"
+          >
+            {{ isSearching ? '搜索中...' : '测试搜索' }}
+          </button>
+        </div>
+
+        <div *ngIf="searchResult" class="result-section">
+          <h4>搜索结果:</h4>
+          <pre>{{ searchResult | json }}</pre>
+        </div>
+      </div>
+
+      <div class="test-section">
+        <h3>视频详情测试</h3>
+        <div class="input-group">
+          <input
+            [(ngModel)]="videoId"
+            placeholder="输入视频ID (aweme_id)"
+            class="test-input"
+          />
+          <button
+            (click)="testVideoDetail()"
+            [disabled]="isGettingDetail"
+            class="test-btn"
+          >
+            {{ isGettingDetail ? '获取中...' : '获取详情' }}
+          </button>
+        </div>
+
+        <div *ngIf="videoDetail" class="result-section">
+          <h4>视频详情:</h4>
+          <pre>{{ videoDetail | json }}</pre>
+        </div>
+      </div>
+
+      <div class="test-section">
+        <h3>Token状态</h3>
+        <div class="token-status">
+          <p>当前Token: <code>{{ currentToken }}</code></p>
+          <p class="status" [class.active]="tokenActive">状态: {{ tokenActive ? '✅ 活跃' : '❌ 未配置' }}</p>
+        </div>
+      </div>
+    </div>
+  `,
+  styles: [`
+    .api-test-container {
+      padding: 20px;
+      max-width: 800px;
+      margin: 0 auto;
+    }
+
+    .test-section {
+      margin-bottom: 30px;
+      padding: 20px;
+      border: 1px solid #ddd;
+      border-radius: 8px;
+    }
+
+    .input-group {
+      display: flex;
+      gap: 10px;
+      margin-bottom: 15px;
+    }
+
+    .test-input {
+      flex: 1;
+      padding: 8px 12px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+    }
+
+    .test-btn {
+      padding: 8px 16px;
+      background: #007bff;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      cursor: pointer;
+    }
+
+    .test-btn:disabled {
+      background: #ccc;
+      cursor: not-allowed;
+    }
+
+    .result-section {
+      margin-top: 15px;
+    }
+
+    pre {
+      background: #f5f5f5;
+      padding: 15px;
+      border-radius: 4px;
+      overflow-x: auto;
+      max-height: 300px;
+      overflow-y: auto;
+    }
+
+    .token-status {
+      background: #f8f9fa;
+      padding: 15px;
+      border-radius: 4px;
+    }
+
+    .status {
+      margin-top: 10px;
+    }
+
+    .status.active {
+      color: #28a745;
+    }
+
+    code {
+      background: #e9ecef;
+      padding: 2px 6px;
+      border-radius: 3px;
+      font-family: monospace;
+    }
+  `]
+})
+export class ApiTestComponent {
+  searchKeyword = '科技';
+  videoId = '';
+  isSearching = false;
+  isGettingDetail = false;
+  searchResult: any = null;
+  videoDetail: any = null;
+
+  // 环境配置Token信息
+  currentToken = '1a8mHd5MhAecmTK4Md2LhRfWMTLRYoZOz0LbglqkbdaBu02aIn6FZjagKw==';
+  tokenActive = true;
+
+  constructor(private douyinApiService: DouyinApiService) {}
+
+  testSearch() {
+    if (!this.searchKeyword.trim() || this.isSearching) return;
+
+    this.isSearching = true;
+    this.searchResult = null;
+
+    this.douyinApiService.searchVideos({
+      keyword: this.searchKeyword,
+      cursor: 0,
+      sort_type: '0',
+      publish_time: '0',
+      filter_duration: '0',
+      content_type: '0'
+    }).subscribe({
+      next: (result) => {
+        this.searchResult = result;
+        this.isSearching = false;
+
+        // 如果有结果,自动填充第一个视频的ID用于详情测试
+        if (result && result.data && result.data.length > 0) {
+          this.videoId = result.data[0].aweme_id;
+        }
+      },
+      error: (error) => {
+        console.error('搜索测试失败:', error);
+        this.searchResult = { error: error.message };
+        this.isSearching = false;
+      }
+    });
+  }
+
+  testVideoDetail() {
+    if (!this.videoId.trim() || this.isGettingDetail) return;
+
+    this.isGettingDetail = true;
+    this.videoDetail = null;
+
+    this.douyinApiService.getVideoDetail(this.videoId).subscribe({
+      next: (result) => {
+        this.videoDetail = result;
+        this.isGettingDetail = false;
+      },
+      error: (error) => {
+        console.error('详情获取失败:', error);
+        this.videoDetail = { error: error.message };
+        this.isGettingDetail = false;
+      }
+    });
+  }
+}

+ 289 - 0
src/app/components/debug-panel/debug-panel.component.ts

@@ -0,0 +1,289 @@
+import { Component, signal } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { DouyinService } from '../../services/douyin.service';
+
+@Component({
+  selector: 'app-debug-panel',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  template: `
+    <div class="debug-panel">
+      <h2>🔧 API调试面板</h2>
+
+      <div class="debug-section">
+        <h3>Token状态</h3>
+        <div class="token-info">
+          <p><strong>当前Token:</strong></p>
+          <code>{{ currentToken }}</code>
+          <button (click)="testToken()" class="debug-btn">测试Token</button>
+        </div>
+        <div *ngIf="tokenTestResult" class="test-result">
+          <strong>测试结果:</strong>
+          <pre>{{ tokenTestResult | json }}</pre>
+        </div>
+      </div>
+
+      <div class="debug-section">
+        <h3>搜索测试</h3>
+        <div class="input-group">
+          <input
+            [(ngModel)]="searchKeyword"
+            placeholder="搜索关键词"
+            class="debug-input"
+          />
+          <button
+            (click)="debugSearch()"
+            [disabled]="isSearching"
+            class="debug-btn"
+          >
+            {{ isSearching ? '搜索中...' : '调试搜索' }}
+          </button>
+        </div>
+
+        <div *ngIf="searchResult" class="test-result">
+          <strong>搜索结果:</strong>
+          <pre>{{ searchResult | json }}</pre>
+        </div>
+
+        <div *ngIf="searchError" class="error-result">
+          <strong>搜索错误:</strong>
+          <pre>{{ searchError }}</pre>
+        </div>
+      </div>
+
+      <div class="debug-section">
+        <h3>网络诊断</h3>
+        <button (click)="testNetwork()" class="debug-btn">测试网络连接</button>
+        <div *ngIf="networkStatus" class="test-result">
+          <strong>网络状态:</strong>
+          <p>{{ networkStatus }}</p>
+        </div>
+      </div>
+
+      <div class="debug-section">
+        <h3>CORS测试</h3>
+        <button (click)="testCORS()" class="debug-btn">测试CORS支持</button>
+        <div *ngIf="corsStatus" class="test-result">
+          <strong>CORS状态:</strong>
+          <p>{{ corsStatus }}</p>
+        </div>
+      </div>
+    </div>
+  `,
+  styles: [`
+    .debug-panel {
+      padding: 20px;
+      max-width: 800px;
+      margin: 0 auto;
+      font-family: Arial, sans-serif;
+    }
+
+    .debug-section {
+      margin-bottom: 30px;
+      padding: 20px;
+      border: 1px solid #ddd;
+      border-radius: 8px;
+      background: #f9f9f9;
+    }
+
+    .debug-section h3 {
+      margin-top: 0;
+      color: #333;
+    }
+
+    .input-group {
+      display: flex;
+      gap: 10px;
+      margin: 15px 0;
+    }
+
+    .debug-input {
+      flex: 1;
+      padding: 8px 12px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      font-size: 14px;
+    }
+
+    .debug-btn {
+      padding: 8px 16px;
+      background: #007bff;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      cursor: pointer;
+      font-size: 14px;
+    }
+
+    .debug-btn:disabled {
+      background: #ccc;
+      cursor: not-allowed;
+    }
+
+    .debug-btn:hover:not(:disabled) {
+      background: #0056b3;
+    }
+
+    .token-info {
+      margin: 10px 0;
+    }
+
+    .token-info code {
+      display: block;
+      background: #f0f0f0;
+      padding: 10px;
+      border-radius: 4px;
+      margin: 10px 0;
+      word-break: break-all;
+      font-size: 12px;
+    }
+
+    .test-result {
+      margin-top: 15px;
+    }
+
+    .test-result pre {
+      background: #e8f5e8;
+      padding: 15px;
+      border-radius: 4px;
+      overflow-x: auto;
+      max-height: 300px;
+      overflow-y: auto;
+      border-left: 4px solid #28a745;
+      font-size: 12px;
+    }
+
+    .error-result {
+      margin-top: 15px;
+    }
+
+    .error-result pre {
+      background: #ffe6e6;
+      padding: 15px;
+      border-radius: 4px;
+      overflow-x: auto;
+      border-left: 4px solid #dc3545;
+      color: #721c24;
+      font-size: 12px;
+    }
+  `]
+})
+export class DebugPanelComponent {
+  searchKeyword = '科技';
+  isSearching = false;
+  searchResult: any = null;
+  searchError: any = null;
+  tokenTestResult: any = null;
+  networkStatus = '';
+  corsStatus = '';
+
+  // 硬编码Token
+  currentToken = 'r:a5a19ea9868043b15d9b10423234ca43';
+
+  constructor(private douyinService: DouyinService) {}
+
+  testToken() {
+    this.tokenTestResult = null;
+
+    // 简单的Token格式验证
+    if (this.currentToken.startsWith('r:') && this.currentToken.length > 10) {
+      this.tokenTestResult = {
+        status: '格式正确',
+        format: 'Bearer Token',
+        prefix: 'r:',
+        length: this.currentToken.length,
+        message: 'Token格式看起来正确'
+      };
+    } else {
+      this.tokenTestResult = {
+        status: '格式错误',
+        message: 'Token格式不符合预期'
+      };
+    }
+  }
+
+  debugSearch() {
+    if (!this.searchKeyword.trim() || this.isSearching) return;
+
+    this.isSearching = true;
+    this.searchResult = null;
+    this.searchError = null;
+
+    console.log('开始调试搜索:', {
+      keyword: this.searchKeyword,
+      service: this.douyinService
+    });
+
+    this.douyinService.searchVideos(
+      this.searchKeyword,
+      '0', // sortType
+      0,   // cursor
+      '0', // publishTime
+      '0', // filterDuration
+      '0'  // contentType
+    ).subscribe({
+      next: (result) => {
+        console.log('搜索成功:', result);
+        this.searchResult = result;
+        this.isSearching = false;
+      },
+      error: (error) => {
+        console.error('搜索失败详细错误:', error);
+        this.searchError = {
+          message: error.message,
+          status: error.status,
+          statusText: error.statusText,
+          name: error.name,
+          stack: error.stack,
+          fullError: error
+        };
+        this.isSearching = false;
+      }
+    });
+  }
+
+  async testNetwork() {
+    this.networkStatus = '正在测试网络连接...';
+
+    try {
+      const startTime = Date.now();
+      const response = await fetch('https://www.baidu.com', {
+        method: 'HEAD',
+        mode: 'no-cors'
+      });
+      const endTime = Date.now();
+
+      this.networkStatus = `网络连接正常 ✅\n响应时间: ${endTime - startTime}ms`;
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误';
+      this.networkStatus = `网络连接异常 ❌\n错误: ${errorMessage}`;
+    }
+  }
+
+  async testCORS() {
+    this.corsStatus = '正在测试CORS支持...';
+
+    try {
+      const response = await fetch('https://server.fmode.cn/api/voc-social/douyin/search/fetch_general_search_v2', {
+        method: 'OPTIONS',
+        headers: {
+          'Origin': window.location.origin,
+          'Access-Control-Request-Method': 'POST',
+          'Access-Control-Request-Headers': 'content-type,authorization'
+        }
+      });
+
+      const corsHeaders = {
+        'Access-Control-Allow-Origin': response.headers.get('Access-Control-Allow-Origin'),
+        'Access-Control-Allow-Methods': response.headers.get('Access-Control-Allow-Methods'),
+        'Access-Control-Allow-Headers': response.headers.get('Access-Control-Allow-Headers')
+      };
+
+      this.corsStatus = `CORS预检请求完成\n状态: ${response.status}\n响应头: ${JSON.stringify(corsHeaders, null, 2)}`;
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误';
+      this.corsStatus = `CORS测试失败 ❌\n错误: ${errorMessage}\n这可能是导致API调用失败的原因`;
+    }
+  }
+}

+ 65 - 0
src/app/components/workflow/workflow.component.ts

@@ -0,0 +1,65 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { JimengWorkflowService } from '../../services/jimeng-workflow.service';
+
+@Component({
+  selector: 'app-workflow',
+  standalone: true,
+  imports: [CommonModule, FormsModule],
+  template: `
+    <div class="container mx-auto p-6">
+      <h2 class="text-2xl font-bold mb-4">逐字稿 → AI视频生成工作流</h2>
+
+      <div class="mb-6">
+        <label class="block mb-2 font-medium">输入逐字稿文本:</label>
+        <textarea
+          [(ngModel)]="transcript"
+          class="w-full p-3 border rounded h-40"
+          placeholder="请输入视频旁白/文字脚本..."></textarea>
+      </div>
+
+      <button
+        (click)="startWorkflow()"
+        [disabled]="isProcessing || !transcript"
+        class="bg-blue-600 text-white px-6 py-2 rounded disabled:bg-gray-400">
+        {{ isProcessing ? 'AI 生成中 (请耐心等待)...' : '一键生成视频素材' }}
+      </button>
+
+      <div *ngIf="results.length > 0" class="mt-8">
+        <h3 class="text-xl font-bold mb-4">生成结果:</h3>
+        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
+          <div *ngFor="let res of results" class="border p-4 rounded shadow-sm">
+            <p class="font-bold">分镜 ID: {{ res.id }}</p>
+            <p class="text-sm text-gray-600 mb-2 truncate">{{ res.prompt }}</p>
+            <video *ngIf="!res.error" [src]="res.videoUrl" controls class="w-full rounded"></video>
+            <p *ngIf="res.error" class="text-red-500">生成失败或超时</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  `
+})
+export class WorkflowComponent {
+  transcript: string = '';
+  isProcessing: boolean = false;
+  results: any[] = [];
+
+  constructor(private workflowService: JimengWorkflowService) {}
+
+  startWorkflow() {
+    this.isProcessing = true;
+    this.results = [];
+
+    this.workflowService.executeFullWorkflow(this.transcript).subscribe({
+      next: (res) => {
+        this.results = res;
+        this.isProcessing = false;
+      },
+      error: (err) => {
+        console.error('Workflow failed', err);
+        this.isProcessing = false;
+      }
+    });
+  }
+}

+ 19 - 0
src/app/fetch

@@ -0,0 +1,19 @@
+fetch("http://localhost:4200/api/api/v1/douyin/search/fetch_general_search_v2", {
+  "headers": {
+    "accept": "application/json",
+    "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+    "authorization": "Bearer gqsZHfMWgAiMwV+ITbmZy0qALADWBZVS7QnV7kKJe9CwzgWgJG+7bwK+GQ==",
+    "content-type": "application/json",
+    "sec-ch-ua": "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Microsoft Edge\";v=\"146\"",
+    "sec-ch-ua-mobile": "?0",
+    "sec-ch-ua-platform": "\"Windows\"",
+    "sec-fetch-dest": "empty",
+    "sec-fetch-mode": "cors",
+    "sec-fetch-site": "same-origin"
+  },
+  "referrer": "http://localhost:4200/",
+  "body": "{\"keyword\":\"美食\",\"sort_type\":\"0\",\"cursor\":0,\"publish_time\":\"0\",\"filter_duration\":\"0\",\"content_type\":\"1\",\"search_id\":\"\",\"backtrace\":\"\"}",
+  "method": "POST",
+  "mode": "cors",
+  "credentials": "include"
+});

+ 87 - 0
src/app/models/types.ts

@@ -0,0 +1,87 @@
+// 视频分镜模型
+export interface StoryboardSegment {
+  id: string;
+  act: string;
+  narration: string;
+  type: string;
+  prompt: string;
+  duration: string;
+}
+
+// 任务结果模型
+export interface WorkflowResult {
+  storyboard: StoryboardSegment[];
+  srt: string;
+  segments: {
+    id: string;
+    videoUrl: string;
+    duration: number;
+    prompt: string;
+  }[];
+  totalSegments: number;
+  successCount: number;
+  failedCount: number;
+  failedIds: string[];
+}
+
+// 抖音视频基础信息
+export interface DouyinVideo {
+  aweme_id: string;
+  desc: string;
+  author: {
+    nickname: string;
+    sec_uid: string;
+    avatar_thumb?: {
+      url_list?: string[];
+    };
+  };
+  statistics: {
+    digg_count: number;
+    play_count: number;
+    comment_count: number;
+    share_count: number;
+  };
+  video?: {
+    play_addr?: {
+      url_list?: string[];
+      uri?: string;
+    };
+    play_addr_265?: {
+      url_list?: string[];
+    };
+    bit_rate?: Array<{
+      play_addr?: {
+        url_list?: string[];
+      };
+    }>;
+    duration?: number;
+    ratio?: string;
+  };
+  download_addr?: {
+    url_list?: string[];
+  };
+  text_extra?: Array<{
+    hashtag_name?: string;
+  }>;
+  create_time?: number;
+}
+
+// 视频搜索结果
+export interface VideoSearchResult {
+  aweme_list?: DouyinVideo[];
+  has_more?: boolean;
+  cursor?: number;
+}
+
+// 视频下载任务
+export interface VideoDownloadTask {
+  id: string;
+  aweme_id: string;
+  title: string;
+  author: string;
+  downloadUrl?: string;
+  status: 'pending' | 'downloading' | 'completed' | 'failed';
+  progress: number;
+  error?: string;
+  fileSize?: number;
+}

+ 16 - 0
src/app/pipes/file-size.pipe.ts

@@ -0,0 +1,16 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'fileSize'
+})
+export class FileSizePipe implements PipeTransform {
+  transform(bytes: number): string {
+    if (bytes === 0) return '0 Bytes';
+
+    const k = 1024;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+  }
+}

+ 39 - 0
src/app/pipes/number-format.pipe.ts

@@ -0,0 +1,39 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'numberFormat',
+  standalone: true
+})
+export class NumberFormatPipe implements PipeTransform {
+
+  transform(value: number | string): string {
+    if (value === null || value === undefined) {
+      return '--';
+    }
+
+    const number = typeof value === 'string' ? parseInt(value, 10) : value;
+
+    if (isNaN(number)) {
+      return '--';
+    }
+
+    if (number < 1000) {
+      return number.toString();
+    }
+
+    if (number < 10000) {
+      return (number / 1000).toFixed(1) + 'K';
+    }
+
+    if (number < 1000000) {
+      return (number / 10000).toFixed(1) + 'W';
+    }
+
+    if (number < 100000000) {
+      return (number / 1000000).toFixed(1) + 'M';
+    }
+
+    return (number / 100000000).toFixed(1) + '亿';
+  }
+
+}

+ 41 - 0
src/app/pipes/time-format.pipe.ts

@@ -0,0 +1,41 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'timeFormat',
+  standalone: true
+})
+export class TimeFormatPipe implements PipeTransform {
+
+  transform(value: number | string): string {
+    if (value === null || value === undefined) {
+      return '--';
+    }
+
+    const seconds = typeof value === 'string' ? parseInt(value, 10) : value;
+
+    if (isNaN(seconds)) {
+      return '--';
+    }
+
+    if (seconds < 60) {
+      return `${seconds}秒`;
+    }
+
+    const minutes = Math.floor(seconds / 60);
+    const remainingSeconds = seconds % 60;
+
+    if (minutes < 60) {
+      return remainingSeconds > 0
+        ? `${minutes}分${remainingSeconds}秒`
+        : `${minutes}分`;
+    }
+
+    const hours = Math.floor(minutes / 60);
+    const remainingMinutes = minutes % 60;
+
+    return remainingMinutes > 0
+      ? `${hours}小时${remainingMinutes}分`
+      : `${hours}小时`;
+  }
+
+}

+ 214 - 0
src/app/services/douyin-api.service.ts

@@ -0,0 +1,214 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+import { catchError, retry } from 'rxjs/operators';
+import { environment } from '../../environments/environment';
+
+// 抖音API接口定义
+interface DouyinSearchParams {
+  keyword: string;
+  cursor?: number | string; // 支持数字或字符串
+  sort_type?: string;
+  publish_time?: string;
+  filter_duration?: string;
+  content_type?: string;
+  search_id?: string;
+}
+
+interface DouyinVideoDetailParams {
+  aweme_id: string;
+}
+
+interface DouyinCommentsParams {
+  aweme_id: string;
+  cursor?: number | string;
+  count?: number | string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class DouyinApiService {
+  private readonly BASE_URL = environment.douyinApi.baseUrl;
+  private readonly TIMEOUT = environment.douyinApi.timeout;
+  private readonly MAX_RETRIES = environment.douyinApi.maxRetries;
+
+  constructor(private http: HttpClient) {}
+
+  /**
+   * 抖音综合搜索
+   * @param params 搜索参数
+   * @returns 搜索结果
+   */
+  searchVideos(params: DouyinSearchParams): Observable<any> {
+    const url = `${this.BASE_URL}/api/v1/douyin/search/fetch_challenge_search_v2`;
+
+    const defaultParams = {
+      cursor: 0,
+      sort_type: '0',
+      publish_time: '0',
+      filter_duration: '0',
+      content_type: '0',
+      search_id: ''
+    };
+
+    const requestParams = {
+      ...defaultParams,
+      ...params,
+      cursor: params.cursor || 0 // 保持cursor为数字,根据API文档
+    };
+
+    return this.http.post(url, requestParams, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 获取单个视频详情
+   * @param aweme_id 视频ID
+   * @returns 视频详情
+   */
+  getVideoDetail(aweme_id: string): Observable<any> {
+    const url = `${this.BASE_URL}/api/v1/douyin/app/v3/fetch_one_video_v3`;
+
+    return this.http.get(url, {
+      params: { aweme_id },
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 获取视频评论
+   * @param params 评论参数
+   * @returns 评论列表
+   */
+  getVideoComments(params: DouyinCommentsParams): Observable<any> {
+    const url = `${this.BASE_URL}/app/v3/fetch_video_comments`;
+
+    const defaultParams = {
+      cursor: 0,
+      count: 20
+    };
+
+    const requestParams = {
+      ...defaultParams,
+      ...params,
+      cursor: params.cursor || 0,
+      count: params.count || 20
+    };
+
+    return this.http.get(url, {
+      params: requestParams,
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 获取评论回复
+   * @param item_id 视频ID
+   * @param comment_id 评论ID
+   * @param cursor 游标
+   * @returns 回复列表
+   */
+  getCommentReplies(item_id: string, comment_id: string, cursor: number = 0): Observable<any> {
+    const url = `${this.BASE_URL}/comment/reply/list`;
+
+    return this.http.post(url, {
+      item_id,
+      comment_id,
+      cursor,
+      count: 20
+    }, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 获取用户信息
+   * @param sec_uid 用户sec_uid
+   * @returns 用户信息
+   */
+  getUserProfile(sec_uid: string): Observable<any> {
+    const url = `${this.BASE_URL}/app/v3/handler_user_profile`;
+
+    return this.http.get(url, {
+      params: { sec_user_id: sec_uid },
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 获取请求头(使用环境配置)
+   */
+  private getHeaders(): HttpHeaders {
+    return new HttpHeaders({
+      'Content-Type': 'application/json',
+      'Accept': 'application/json',
+      'Authorization': `Bearer ${environment.douyinApi.token}`
+    });
+  }
+
+  /**
+   * 错误处理
+   */
+  private handleError(error: any): Observable<never> {
+    let errorMessage = '';
+
+    if (error.error instanceof ErrorEvent) {
+      // 客户端错误
+      errorMessage = `网络错误: ${error.error.message}`;
+    } else {
+      // 服务端错误
+      switch (error.status) {
+        case 401:
+          errorMessage = '认证失败 (401): Token无效或已过期';
+          break;
+        case 403:
+          errorMessage = '访问被拒绝 (403): 权限不足';
+          break;
+        case 429:
+          errorMessage = '请求过于频繁 (429): 请稍后重试';
+          break;
+        case 500:
+          errorMessage = '服务器内部错误 (500): 请稍后重试';
+          break;
+        case 400:
+          errorMessage = '请求参数错误 (400): 请检查请求参数格式';
+          break;
+        case 0:
+          errorMessage = '网络连接失败: 请检查网络设置';
+          break;
+        default:
+          errorMessage = `请求失败 (${error.status}): ${error.message || '未知错误'}`;
+      }
+
+      // 检查API返回的具体错误信息
+      if (error.error && error.error.msg) {
+        errorMessage += ` | API错误: ${error.error.msg}`;
+      }
+    }
+
+    console.error('Douyin API Error:', errorMessage, error);
+    return throwError(() => new Error(errorMessage));
+  }
+}

+ 477 - 0
src/app/services/douyin.service.ts

@@ -0,0 +1,477 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, throwError, timer } from 'rxjs';
+import { catchError, retry } from 'rxjs/operators';
+import { DouyinVideo, VideoDownloadTask } from '../models/types';
+import { environment } from '../../environments/environment';
+
+@Injectable({ providedIn: 'root' })
+export class DouyinService {
+  private readonly baseUrl = environment.douyinApi.baseUrl;
+  private readonly TIMEOUT = environment.douyinApi.timeout;
+  private readonly MAX_RETRIES = environment.douyinApi.maxRetries;
+
+  constructor(private http: HttpClient) {}
+
+  // 抖音话题搜索 V2
+  searchVideos(
+    keyword: string,
+    sortType: string = '0',
+    cursor: number = 0,
+    publishTime: string = '0',
+    filterDuration: string = '0',
+    contentType: string = '0',
+    searchId: string = '',
+    backtrace: string = ''
+  ): Observable<any> {
+    const requestBody = {
+      keyword: keyword || '',
+      sort_type: sortType || '0',
+      cursor: cursor, // 保持为数字,根据API文档示例
+      publish_time: publishTime || '0',
+      filter_duration: filterDuration || '0',
+      content_type: contentType || '0',
+      search_id: searchId || '',
+      backtrace: backtrace || ''
+    };
+
+    console.log('🔍 发送搜索请求:', {
+      url: `${this.baseUrl}/api/v1/douyin/search/fetch_general_search_v2`,
+      body: requestBody,
+      headers: this.getHeaders()
+    });
+
+    return this.http.post(`${this.baseUrl}/api/v1/douyin/search/fetch_general_search_v2`, requestBody, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry({
+        count: this.MAX_RETRIES,
+        delay: (error, retryCount) => {
+          console.log(`请求失败,第${retryCount}次重试...`, error);
+          return timer(retryCount * 1000); // 递增延迟
+        }
+      }),
+      catchError(this.handleError)
+    );
+  }
+
+  // 获取视频详情
+  getVideoDetail(awemeId: string): Observable<any> {
+    return this.http.get(`${this.baseUrl}/api/v1/douyin/app/v3/fetch_one_video_v3`, {
+      params: { aweme_id: awemeId },
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  getUserProfileByUniqueId(uniqueId: string): Observable<any> {
+    return this.http.get(`${this.baseUrl}/api/v1/douyin/web/handler_user_profile_v2`, {
+      params: { unique_id: uniqueId },
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  getUserProfileBySecUserId(secUserId: string): Observable<any> {
+    return this.http.get(`${this.baseUrl}/api/v1/douyin/app/v3/handler_user_profile`, {
+      params: { sec_user_id: secUserId },
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  getUserPostVideos(secUserId: string, maxCursor: string = '0', count: number = 18, filterType: string = '0'): Observable<any> {
+    return this.http.get(`${this.baseUrl}/api/v1/douyin/web/fetch_user_post_videos`, {
+      params: {
+        sec_user_id: secUserId,
+        max_cursor: maxCursor,
+        count,
+        filter_type: filterType
+      },
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  extractUserProfileData(payload: any): any {
+    const normalizedResponse = this.parseJsonPayload(payload);
+    const normalizedData = this.parseJsonPayload(normalizedResponse?.data);
+    const nestedData = this.parseJsonPayload(normalizedData?.data);
+
+    return this.parseJsonPayload(
+      normalizedData?.user_info
+      || normalizedData?.user
+      || nestedData?.user_info
+      || nestedData?.user
+      || nestedData
+      || normalizedData
+      || null
+    );
+  }
+
+  extractUserPostVideos(payload: any): { items: any[]; hasMore: boolean; cursor: string } {
+    const normalizedResponse = this.parseJsonPayload(payload);
+    const normalizedData = this.parseJsonPayload(normalizedResponse?.data);
+    const nestedData = this.parseJsonPayload(normalizedData?.data);
+    const source = nestedData || normalizedData || normalizedResponse || {};
+    const items = Array.isArray(source?.aweme_list)
+      ? source.aweme_list
+      : Array.isArray(source?.items)
+        ? source.items
+        : [];
+    const hasMore = source?.has_more === 1 || source?.has_more === true;
+    const cursor = String(source?.max_cursor ?? source?.cursor ?? '0');
+
+    return { items, hasMore, cursor };
+  }
+
+  extractVideoDetailData(videoData: any): any {
+    const normalizedResponse = this.parseJsonPayload(videoData);
+    const normalizedData = this.parseJsonPayload(normalizedResponse?.data);
+    const nestedData = this.parseJsonPayload(normalizedData?.data);
+
+    return this.parseJsonPayload(
+      normalizedData?.aweme_detail
+      || normalizedData?.aweme_info
+      || normalizedData?.aweme_details?.[0]
+      || nestedData?.aweme_detail
+      || nestedData?.aweme_info
+      || nestedData?.aweme_details?.[0]
+      || nestedData
+      || normalizedData
+      || null
+    );
+  }
+
+  extractVideoPreviewUrl(videoData: any): string | null {
+    const detailData = this.extractVideoDetailData(videoData);
+
+    if (!detailData) {
+      return null;
+    }
+
+    return this.pickPreferredUrl(this.collectPreviewUrls(detailData));
+  }
+
+  extractVideoDownloadUrl(videoData: any): string | null {
+    return this.pickPreferredUrl(this.extractVideoDownloadUrls(videoData));
+  }
+
+  extractVideoDownloadUrls(videoData: any): string[] {
+    const detailData = this.extractVideoDetailData(videoData);
+
+    if (!detailData) {
+      return [];
+    }
+
+    const downloadCandidates = [
+      ...(Array.isArray(detailData.download_addr?.url_list) ? detailData.download_addr.url_list : []),
+      ...(Array.isArray(detailData.video?.download_addr?.url_list) ? detailData.video.download_addr.url_list : []),
+      ...(Array.isArray(detailData.video?.download_suffix_logo_addr?.url_list) ? detailData.video.download_suffix_logo_addr.url_list : [])
+    ];
+
+    return this.pickUniqueUrls([...downloadCandidates, ...this.collectPlayableUrls(detailData)]);
+  }
+
+  startManagedDownload(payload: {
+    url: string;
+    urls?: string[];
+    filename: string;
+    title: string;
+    description: string;
+    tags: string[];
+    thumbnail: string;
+    duration: number;
+    resolution: string;
+    awemeId: string;
+    authorName: string;
+  }): Observable<any> {
+    return this.http.post('/backend/api/download/video', payload);
+  }
+
+  getManagedDownloadTask(taskId: string): Observable<any> {
+    return this.http.get(`/backend/api/download/video/${taskId}`);
+  }
+
+  private collectPreviewUrls(detailData: any): string[] {
+    const prioritizedUrls = [
+      ...(Array.isArray(detailData.video?.play_addr_lowbr?.url_list) ? detailData.video.play_addr_lowbr.url_list : []),
+      ...(Array.isArray(detailData.video?.play_addr_h264?.url_list) ? detailData.video.play_addr_h264.url_list : []),
+      ...(Array.isArray(detailData.video?.play_addr?.url_list) ? detailData.video.play_addr.url_list : []),
+      ...(Array.isArray(detailData.video?.play_addr_265?.url_list) ? detailData.video.play_addr_265.url_list : [])
+    ];
+
+    return this.pickUniqueUrls([...prioritizedUrls, ...this.collectPlayableUrls(detailData)]);
+  }
+
+  private collectPlayableUrls(detailData: any): string[] {
+    const bitRateUrls = Array.isArray(detailData.video?.bit_rate)
+      ? detailData.video.bit_rate.reduce((urls: string[], item: any) => {
+          const currentUrls = Array.isArray(item?.play_addr?.url_list) ? item.play_addr.url_list : [];
+          return urls.concat(currentUrls);
+        }, [])
+      : [];
+
+    const directUrls = [
+      ...(Array.isArray(detailData.video?.play_addr?.url_list) ? detailData.video.play_addr.url_list : []),
+      ...(Array.isArray(detailData.video?.play_addr_h264?.url_list) ? detailData.video.play_addr_h264.url_list : []),
+      ...(Array.isArray(detailData.video?.play_addr_265?.url_list) ? detailData.video.play_addr_265.url_list : []),
+      ...(Array.isArray(detailData.video?.play_addr_lowbr?.url_list) ? detailData.video.play_addr_lowbr.url_list : []),
+      ...(Array.isArray(detailData.video?.download_addr?.url_list) ? detailData.video.download_addr.url_list : []),
+      ...(Array.isArray(detailData.download_addr?.url_list) ? detailData.download_addr.url_list : []),
+      detailData.video?.play_addr?.uri,
+      detailData.video?.download_addr?.uri
+    ];
+
+    return this.pickUniqueUrls([...directUrls, ...bitRateUrls]);
+  }
+
+  private pickUniqueUrls(urls: any[]): string[] {
+    return urls.filter((url, index, list) => (
+      typeof url === 'string'
+      && /^https?:\/\//i.test(url)
+      && list.indexOf(url) === index
+    ));
+  }
+
+  private pickPreferredUrl(urls: any[]): string | null {
+    const validUrls = urls.filter(url => typeof url === 'string' && url.trim() !== '');
+    const preferredUrl = validUrls.find(url => !/watermark|playwm|download_suffix_logo/i.test(url));
+
+    return preferredUrl || validUrls[0] || null;
+  }
+
+  private parseJsonPayload(value: any): any {
+    let current = value;
+
+    while (typeof current === 'string') {
+      const trimmed = current.trim();
+      if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
+        break;
+      }
+
+      try {
+        current = JSON.parse(trimmed);
+      } catch {
+        break;
+      }
+    }
+
+    return current;
+  }
+
+  // 下载视频到本地(改进版本,支持进度跟踪)
+  async downloadVideo(videoUrl: string, filename: string, onProgress?: (progress: number) => void): Promise<void> {
+    try {
+      const response = await fetch(videoUrl, {
+        method: 'GET',
+        headers: {
+          'Accept': 'video/mp4,*/*',
+          'Cache-Control': 'no-cache'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error(`下载失败: ${response.status} ${response.statusText}`);
+      }
+
+      const contentLength = response.headers.get('content-length');
+      const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
+
+      const reader = response.body?.getReader();
+      if (!reader) {
+        throw new Error('无法读取视频流');
+      }
+
+      const chunks: Uint8Array[] = [];
+      let receivedBytes = 0;
+
+      while (true) {
+        const { done, value } = await reader.read();
+
+        if (done) break;
+
+        chunks.push(value);
+        receivedBytes += value.length;
+
+        if (onProgress && totalBytes > 0) {
+          const progress = Math.round((receivedBytes / totalBytes) * 100);
+          onProgress(progress);
+        }
+      }
+
+      // 合并所有数据块
+      const blob = new Blob(chunks as BlobPart[], { type: 'video/mp4' });
+
+      // 创建下载链接
+      const url = window.URL.createObjectURL(blob);
+      const link = document.createElement('a');
+      link.href = url;
+      link.download = filename;
+      link.style.display = 'none';
+
+      document.body.appendChild(link);
+      link.click();
+
+      // 清理
+      setTimeout(() => {
+        document.body.removeChild(link);
+        window.URL.revokeObjectURL(url);
+      }, 100);
+
+      if (onProgress) {
+        onProgress(100);
+      }
+    } catch (error) {
+      console.error('视频下载错误:', error);
+      throw error;
+    }
+  }
+
+  // 从URL中提取aweme_id(增强版本)
+  extractAwemeId(url: string): string | null {
+    if (!url) return null;
+
+    // 处理多种抖音链接格式
+    const patterns = [
+      /douyin\.com\/video\/(\d+)/,                    // https://www.douyin.com/video/1234567890
+      /douyin\.com\/\w+\/(\d+)/,                      // https://www.douyin.com/user/123/71891234567890
+      /aweme_id=(\d+)/,                                // aweme_id=1234567890
+      /^(\d+)$/,                                        // 纯数字ID
+      /douyin\.com\/\?modal_id=(\d+)/,                 // 分享链接格式
+      /v\.douyin\.com\/\w+\?id=(\d+)/,                 // 短链接格式
+      /douyin\.com\/share\/video\/(\d+)/               // 分享页面格式
+    ];
+
+    for (const pattern of patterns) {
+      const match = url.match(pattern);
+      if (match) {
+        return match[1];
+      }
+    }
+
+    return null;
+  }
+
+  // 批量获取视频信息
+  getBatchVideoDetails(awemeIds: string[]): Observable<any>[] {
+    return awemeIds.map(id => this.getVideoDetail(id));
+  }
+
+  // 验证视频数据完整性
+  validateVideoData(videoData: any): boolean {
+    if (!videoData || !videoData.data) {
+      return false;
+    }
+
+    const data = videoData.data;
+    return !!(data.aweme_id && data.desc !== undefined && data.author);
+  }
+
+  // 格式化视频信息用于显示
+  formatVideoInfo(videoData: DouyinVideo): DouyinVideo {
+    return {
+      ...videoData,
+      statistics: {
+        digg_count: videoData.statistics?.digg_count || 0,
+        play_count: videoData.statistics?.play_count || 0,
+        comment_count: videoData.statistics?.comment_count || 0,
+        share_count: videoData.statistics?.share_count || 0
+      },
+      author: {
+        nickname: videoData.author?.nickname || '未知作者',
+        sec_uid: videoData.author?.sec_uid || '',
+        avatar_thumb: videoData.author?.avatar_thumb
+      }
+    };
+  }
+
+  // 生成安全的文件名
+  generateSafeFilename(video: DouyinVideo): string {
+    const author = video.author?.nickname || '未知作者';
+    const title = video.desc || `视频_${video.aweme_id}`;
+    const safeTitle = title
+      .replace(/[<>:"/\\|?*]/g, '_')  // 移除Windows文件名非法字符
+      .replace(/\s+/g, '_')           // 空格替换为下划线
+      .substring(0, 50);              // 限制标题长度
+
+    return `${author}_${safeTitle}_${video.aweme_id}.mp4`;
+  }
+
+  // 获取请求头
+  private getHeaders(): HttpHeaders {
+    return new HttpHeaders({
+      'Content-Type': 'application/json',
+      'Accept': 'application/json',
+      'Authorization': `Bearer ${environment.douyinApi.token}`
+    });
+  }
+
+  // 错误处理
+  private handleError(error: any): Observable<never> {
+    let errorMessage = '';
+
+    if (error.error instanceof ErrorEvent) {
+      // 客户端错误
+      errorMessage = `网络错误: ${error.error.message}`;
+    } else {
+      // 服务端错误
+      switch (error.status) {
+        case 401:
+          errorMessage = '认证失败 (401): Token无效或已过期';
+          break;
+        case 403:
+          errorMessage = '访问被拒绝 (403): 权限不足';
+          break;
+        case 429:
+          errorMessage = '请求过于频繁 (429): 请稍后重试';
+          break;
+        case 500:
+          errorMessage = '服务器内部错误 (500): 请稍后重试';
+          break;
+        case 400:
+          errorMessage = '请求参数错误 (400): 请检查请求参数格式';
+          break;
+        case 0:
+          errorMessage = '网络连接失败: 请检查网络设置';
+          break;
+        default:
+          errorMessage = `请求失败 (${error.status}): ${error.message || '未知错误'}`;
+      }
+
+      // 检查API返回的具体错误信息
+      if (error.error) {
+        console.log('🔍 详细错误信息:', {
+          status: error.status,
+          statusText: error.statusText,
+          error: error.error,
+          message: error.message,
+          url: error.url
+        });
+
+        if (error.error.msg) {
+          errorMessage += ` | API错误: ${error.error.msg}`;
+        }
+        if (error.error.data) {
+          errorMessage += ` | 错误数据: ${JSON.stringify(error.error.data)}`;
+        }
+      }
+    }
+
+    console.error('Douyin API Error:', errorMessage, error);
+    return throwError(() => new Error(errorMessage));
+  }
+}

+ 264 - 0
src/app/services/jimeng-api.service.ts

@@ -0,0 +1,264 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, throwError, timer } from 'rxjs';
+import { catchError, retry, switchMap } from 'rxjs/operators';
+
+// 即梦AI API接口定义
+interface JimengVideoGenerationParams {
+  prompt: string;
+  image_url?: string;
+  image_urls?: string | string[];
+  frames?: number;
+  aspect_ratio?: number;
+  method?: 'text2video' | 'image2video';
+  token?: string;
+}
+
+interface JimengImageGenerationParams {
+  prompt: string;
+  sizeData?: {
+    width: number;
+    height: number;
+  };
+  token?: string;
+}
+
+interface JimengTaskQueryParams {
+  task_id: string;
+  token?: string;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class JimengApiService {
+  private readonly BASE_URL = 'https://server.fmode.cn/api/volcengine/jimeng';
+  private readonly TIMEOUT = 60000;
+  private readonly MAX_RETRIES = 3;
+
+  constructor(private http: HttpClient) {}
+
+  /**
+   * 文生视频 (高质量版本)
+   * @param params 生成参数
+   * @returns 任务ID
+   */
+  generateVideoPro(params: JimengVideoGenerationParams): Observable<any> {
+    const url = `${this.BASE_URL}/getVideoV3_Pro`;
+
+    const defaultParams = {
+      frames: 121,
+      aspect_ratio: 1, // 16:9
+      method: 'text2video',
+      token: this.getToken()
+    };
+
+    const requestParams = { ...defaultParams, ...params };
+
+    return this.http.post(url, requestParams, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 文生视频 (720p版本)
+   * @param params 生成参数
+   * @returns 任务ID
+   */
+  generateVideo720p(params: JimengVideoGenerationParams): Observable<any> {
+    const url = `${this.BASE_URL}/getVideoV3_720p`;
+
+    const defaultParams = {
+      frames: 121,
+      aspect_ratio: 1,
+      method: 'text2video',
+      token: this.getToken()
+    };
+
+    const requestParams = { ...defaultParams, ...params };
+
+    return this.http.post(url, requestParams, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 文生图片 (v31版本)
+   * @param params 生成参数
+   * @returns 任务ID
+   */
+  generateImageV31(params: JimengImageGenerationParams): Observable<any> {
+    const url = `${this.BASE_URL}/getText2ImgV31`;
+
+    const defaultParams = {
+      sizeData: {
+        width: 1024,
+        height: 1024
+      },
+      token: this.getToken()
+    };
+
+    const requestParams = { ...defaultParams, ...params };
+
+    return this.http.post(url, requestParams, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 查询任务状态
+   * @param task_id 任务ID
+   * @returns 任务状态
+   */
+  queryTaskStatus(task_id: string): Observable<any> {
+    const url = `${this.BASE_URL}/getDataByTask02`;
+
+    const params = {
+      task_id,
+      token: this.getToken()
+    };
+
+    return this.http.post(url, params, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 获取生成结果
+   * @param task_id 任务ID
+   * @returns 生成结果
+   */
+  getWorkResult(task_id: string): Observable<any> {
+    const url = `${this.BASE_URL}/getWorkResult`;
+
+    const params = {
+      task_id,
+      token: this.getToken()
+    };
+
+    return this.http.post(url, params, {
+      headers: this.getHeaders(),
+      timeout: this.TIMEOUT
+    }).pipe(
+      retry(this.MAX_RETRIES),
+      catchError(this.handleError)
+    );
+  }
+
+  /**
+   * 轮询任务状态
+   * @param task_id 任务ID
+   * @param interval 轮询间隔(毫秒)
+   * @param timeout 超时时间(毫秒)
+   * @returns 任务状态流
+   */
+  pollTaskStatus(task_id: string, interval: number = 5000, timeout: number = 1800000): Observable<any> {
+    return timer(0, interval).pipe(
+      switchMap(() => this.queryTaskStatus(task_id)),
+      catchError(error => {
+        console.warn('轮询任务状态失败:', error);
+        return throwError(() => error);
+      })
+    );
+  }
+
+  /**
+   * 监控任务直到完成
+   * @param task_id 任务ID
+   * @returns 最终结果
+   */
+  monitorTaskUntilComplete(task_id: string): Observable<any> {
+    return new Observable(observer => {
+      const interval = setInterval(() => {
+        this.queryTaskStatus(task_id).subscribe({
+          next: (response) => {
+            const status = response.status || response.data?.status;
+
+            if (status === 'completed' || status === 'success') {
+              clearInterval(interval);
+              this.getWorkResult(task_id).subscribe({
+                next: (result) => {
+                  observer.next(result);
+                  observer.complete();
+                },
+                error: (error) => observer.error(error)
+              });
+            } else if (status === 'failed' || status === 'error') {
+              clearInterval(interval);
+              observer.error(new Error('任务执行失败'));
+            }
+            // 继续轮询 pending, processing 状态
+          },
+          error: (error) => {
+            clearInterval(interval);
+            observer.error(error);
+          }
+        });
+      }, 5000);
+
+      // 30分钟后超时
+      const timeoutId = setTimeout(() => {
+        clearInterval(interval);
+        observer.error(new Error('任务执行超时'));
+      }, 1800000);
+
+      // 清理函数
+      return () => {
+        clearInterval(interval);
+        clearTimeout(timeoutId);
+      };
+    });
+  }
+
+  /**
+   * 获取API认证Token
+   */
+  private getToken(): string {
+    // 这里应该从环境变量或安全存储中获取
+    return 'your-jimeng-api-token';
+  }
+
+  /**
+   * 获取请求头
+   */
+  private getHeaders(): HttpHeaders {
+    return new HttpHeaders({
+      'Content-Type': 'application/json',
+      'Accept': 'application/json'
+    });
+  }
+
+  /**
+   * 错误处理
+   */
+  private handleError(error: any): Observable<never> {
+    let errorMessage = '';
+
+    if (error.error instanceof ErrorEvent) {
+      // 客户端错误
+      errorMessage = `客户端错误: ${error.error.message}`;
+    } else {
+      // 服务端错误
+      errorMessage = `服务端错误: ${error.status}\n消息: ${error.message}`;
+    }
+
+    console.error('Jimeng API Error:', errorMessage);
+    return throwError(() => new Error(errorMessage));
+  }
+}

+ 81 - 0
src/app/services/jimeng-workflow.service.ts

@@ -0,0 +1,81 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, from, of, throwError } from 'rxjs';
+import { switchMap, delay, map, catchError, concatMap, toArray } from 'rxjs/operators';
+import { StoryboardSegment } from '../models/types';
+
+@Injectable({ providedIn: 'root' })
+export class JimengWorkflowService {
+  private readonly baseUrl = 'https://server.fmode.cn/api/volcengine/jimeng';
+
+  constructor(private http: HttpClient) {}
+
+  // Stage 1: AI 分镜脚本生成 (此处通常调用后端LLM,前端做Mock演示)
+  generateStoryboard(transcript: string, segmentDuration: number = 15): Observable<StoryboardSegment[]> {
+    // 实际项目中应替换为对您自己Node.js后端的请求
+    return of([
+      {
+        id: "S-01",
+        act: "开场",
+        narration: transcript.substring(0, 45),
+        type: "ai-gen",
+        prompt: "Extreme wide shot: a single desk with a glowing monitor floating in an infinite dark void...",
+        duration: "15s"
+      }
+    ]).pipe(delay(2000));
+  }
+
+  // Stage 2 & 3: 提交视频生成任务并轮询
+  generateVideoSegment(prompt: string, method: string = '1'): Observable<string> {
+    const payload = {
+      prompt,
+      method,
+      frames: 121 // 5秒默认值
+    };
+
+    return this.http.post<any>(`${this.baseUrl}/getVideoV3_720p`, payload).pipe(
+      switchMap(res => {
+        if (!res.workId) return throwError(() => new Error('Task creation failed'));
+        return this.pollTaskStatus(res.workId, 'getVideoV3_720p');
+      })
+    );
+  }
+
+  // Stage 3: 轮询获取结果
+  private pollTaskStatus(workId: string, routerName: string): Observable<string> {
+    return this.http.post<any>(`${this.baseUrl}/getDataByTask02`, { workId, routerName }).pipe(
+      switchMap(res => {
+        if (res.isFinish) {
+          // 任务完成,获取结果URL
+          return this.http.post<any>(`${this.baseUrl}/getWorkResult`, { workId }).pipe(
+            map(result => result.videos[0])
+          );
+        } else {
+          // 建议每3-5秒轮询一次
+          return of(null).pipe(
+            delay(5000),
+            switchMap(() => this.pollTaskStatus(workId, routerName))
+          );
+        }
+      })
+    );
+  }
+
+  // 执行完整链路
+  executeFullWorkflow(transcript: string): Observable<any> {
+    return this.generateStoryboard(transcript).pipe(
+      // Stage 2: 批量生成视频素材 (并行策略:串行或并发控制)
+      switchMap(segments =>
+        from(segments).pipe(
+          concatMap(segment =>
+            this.generateVideoSegment(segment.prompt).pipe(
+              map(url => ({ id: segment.id, videoUrl: url, duration: 5, prompt: segment.prompt })),
+              catchError(err => of({ id: segment.id, error: true }))
+            )
+          ),
+          toArray()
+        )
+      )
+    );
+  }
+}

+ 943 - 0
src/app/services/jimeng.service.ts

@@ -0,0 +1,943 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Observable, Subject, timer, throwError, of } from 'rxjs';
+import { switchMap, takeWhile, tap, catchError, map } from 'rxjs/operators';
+import { environment } from '../../environments/environment';
+
+// MD5 hash function (lightweight, for signing only)
+function md5(input: string): string {
+  function safeAdd(x: number, y: number) {
+    const lsw = (x & 0xffff) + (y & 0xffff);
+    return (((x >> 16) + (y >> 16) + (lsw >> 16)) << 16) | (lsw & 0xffff);
+  }
+  function bitRotateLeft(num: number, cnt: number) {
+    return (num << cnt) | (num >>> (32 - cnt));
+  }
+  function md5cmn(q: number, a: number, b: number, x: number, s: number, t: number) {
+    return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
+  }
+  function md5ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
+    return md5cmn((b & c) | (~b & d), a, b, x, s, t);
+  }
+  function md5gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
+    return md5cmn((b & d) | (c & ~d), a, b, x, s, t);
+  }
+  function md5hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
+    return md5cmn(b ^ c ^ d, a, b, x, s, t);
+  }
+  function md5ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
+    return md5cmn(c ^ (b | ~d), a, b, x, s, t);
+  }
+  function binlMD5(x: number[], len: number): number[] {
+    x[len >> 5] |= 0x80 << (len % 32);
+    x[((len + 64) >>> 9 << 4) + 14] = len;
+    let a = 1732584193, b = -271733879, c = -1732584194, d = 271733878;
+    for (let i = 0; i < x.length; i += 16) {
+      const olda = a, oldb = b, oldc = c, oldd = d;
+      a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
+      c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
+      a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
+      c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
+      a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
+      c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
+      a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
+      c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
+      a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
+      c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302);
+      a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
+      c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
+      a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
+      c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
+      a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
+      c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
+      a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
+      c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
+      a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
+      c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
+      a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222);
+      c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
+      a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
+      c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
+      a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
+      c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
+      a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
+      c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
+      a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
+      c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
+      a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
+      c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
+      a = safeAdd(a, olda); b = safeAdd(b, oldb); c = safeAdd(c, oldc); d = safeAdd(d, oldd);
+    }
+    return [a, b, c, d];
+  }
+  function str2binl(str: string): number[] {
+    const bin: number[] = [];
+    const mask = (1 << 8) - 1;
+    for (let i = 0; i < str.length * 8; i += 8) {
+      bin[i >> 5] |= (str.charCodeAt(i / 8) & mask) << (i % 32);
+    }
+    return bin;
+  }
+  function binl2hex(binarray: number[]): string {
+    const hexTab = '0123456789abcdef';
+    let str = '';
+    for (let i = 0; i < binarray.length * 4; i++) {
+      str += hexTab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xf) +
+             hexTab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xf);
+    }
+    return str;
+  }
+  return binl2hex(binlMD5(str2binl(input), input.length * 8));
+}
+
+export interface JimengVideoRequest {
+  prompt: string;
+  method: '1' | '2' | '3' | '4'; // 1文生视频 2首帧 3首尾帧 4运镜
+  frames?: number; // 121(5s) 或 241(10s)
+  config?: {
+    aspect_ratio?: string;
+    image_urls?: string[];
+    template_id?: string;
+    camera_strength?: string;
+  };
+}
+
+export interface JimengTaskStatus {
+  tip: string;
+  isFinish: boolean;
+  isContinue?: boolean;
+}
+
+export interface JimengWorkResult {
+  objectId: string;
+  images?: string[];
+  videos?: string[];
+  prompt?: string;
+  progress?: number;
+  usage?: any;
+}
+
+export interface RemixTask {
+  id: string;
+  videoTitle: string;
+  style: string;
+  prompt: string;
+  workId?: string;
+  status: 'submitting' | 'generating' | 'polling' | 'completed' | 'failed';
+  progress: number;
+  resultUrl?: string;
+  error?: string;
+  createdAt: Date;
+}
+
+@Injectable({
+  providedIn: 'root'
+})
+export class JimengService {
+  private readonly digitalHumanSubmitRetryableCodes = new Set([50429, 50430, 50500, 50501]);
+  private readonly baseUrl = environment.jimengApi.baseUrl;
+  private readonly parseBaseUrl = environment.jimengApi.parseBaseUrl;
+  private readonly parseAppId = environment.jimengApi.parseAppId;
+  private readonly token = environment.jimengApi.token;
+  private readonly pollInterval = environment.jimengApi.pollIntervalMs;
+  private readonly pollMaxAttempts = environment.jimengApi.pollMaxAttempts;
+  private readonly digitalHumanPollMaxAttempts = Math.max(environment.jimengApi.pollMaxAttempts, 120);
+  private readonly digitalHumanResultCheckInterval = 3;
+
+  constructor(private http: HttpClient) {}
+
+  // 提交文生视频任务 (720p)
+  generateVideo720p(request: JimengVideoRequest): Observable<any> {
+    const body = {
+      prompt: request.prompt,
+      method: request.method,
+      frames: request.frames || 121,
+      config: request.config || { aspect_ratio: '16:9' },
+      token: this.token
+    };
+
+    console.log('🎬 即梦API - 提交视频生成任务:', body);
+
+    return this.http.post(`${this.baseUrl}/getVideoV3_720p`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      tap((res: any) => console.log('🎬 即梦API - 任务提交响应:', res)),
+      catchError(this.handleError('generateVideo720p'))
+    );
+  }
+
+  // 提交文生视频任务 (1080p)
+  generateVideo1080p(request: JimengVideoRequest): Observable<any> {
+    const body = {
+      prompt: request.prompt,
+      method: request.method,
+      frames: request.frames || 121,
+      config: request.config || { aspect_ratio: '16:9' },
+      token: this.token
+    };
+
+    console.log('🎬 即梦API - 提交1080p视频生成任务:', body);
+
+    return this.http.post(`${this.baseUrl}/getVideoV3_1080p`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      tap((res: any) => console.log('🎬 即梦API - 1080p任务提交响应:', res)),
+      catchError(this.handleError('generateVideo1080p'))
+    );
+  }
+
+  // 提交文生视频任务 (Pro)
+  generateVideoPro(request: JimengVideoRequest): Observable<any> {
+    const body = {
+      prompt: request.prompt,
+      method: request.method,
+      frames: request.frames || 121,
+      config: request.config || { aspect_ratio: '16:9' },
+      token: this.token
+    };
+
+    console.log('🎬 即梦API - 提交Pro视频生成任务:', body);
+
+    return this.http.post(`${this.baseUrl}/getVideoV3_Pro`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      tap((res: any) => console.log('🎬 即梦API - Pro任务提交响应:', res)),
+      catchError(this.handleError('generateVideoPro'))
+    );
+  }
+
+  // 提交文生图任务 (v3.1)
+  generateImage(prompt: string, width: number = 1920, height: number = 1080): Observable<any> {
+    const body = {
+      prompt: prompt,
+      sizeDate: { width, height },
+      token: this.token
+    };
+
+    console.log('🖼️ 即梦API - 提交图片生成任务:', body);
+
+    return this.http.post(`${this.baseUrl}/getText2ImgV31`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      tap((res: any) => console.log('🖼️ 即梦API - 图片任务提交响应:', res)),
+      catchError(this.handleError('generateImage'))
+    );
+  }
+
+  // 查询任务状态
+  queryTask(workId: string, routerName: string): Observable<any> {
+    const body = {
+      workId: workId,
+      routerName: routerName,
+      token: this.token
+    };
+
+    return this.http.post(`${this.baseUrl}/getDataByTask02`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      catchError(this.handleError('queryTask'))
+    );
+  }
+
+  // 获取作品结果(通过 Parse API 查询 ImagineWork 表)
+  getWorkResult(workId: string): Observable<JimengWorkResult> {
+    return this.http.get<JimengWorkResult>(
+      `${this.parseBaseUrl}/classes/ImagineWork/${workId}`,
+      {
+        headers: new HttpHeaders({
+          'X-Parse-Application-Id': this.parseAppId
+        })
+      }
+    ).pipe(
+      tap((res: any) => console.log('📦 即梦API - 作品结果:', res)),
+      catchError(this.handleError('getWorkResult'))
+    );
+  }
+
+  uploadFileToParse(file: Blob, filename: string, contentType?: string): Observable<string> {
+    const formData = new FormData();
+    const normalizedFilename = String(filename || `asset-${Date.now()}`);
+    const typedFile = file instanceof File
+      ? file
+      : new File([file], normalizedFilename, { type: contentType || 'application/octet-stream' });
+
+    formData.append('file', typedFile, normalizedFilename);
+
+    return this.http.post<any>(
+      `/backend/api/remix/upload-asset`,
+      formData
+    ).pipe(
+      map((res: any) => {
+        const url = res?.url || '';
+        if (!url) {
+          throw new Error('上传素材失败,未返回可用 URL');
+        }
+
+        return url;
+      }),
+      tap((url: string) => console.log('📤 数字人素材上传成功:', url)),
+      catchError(this.handleError('uploadFileToParse'))
+    );
+  }
+
+  identifyDigitalHuman(imageUrl: string): Observable<{ workId: string }> {
+    const body = {
+      image_url: imageUrl,
+      token: this.token
+    };
+
+    return this.http.post<any>(`${this.baseUrl}/getOhIdentifyMain`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      map((res: any) => {
+        const workId = res?.data?.workId || res?.data?.taskId || '';
+        if (res?.code !== 200 || !workId) {
+          throw new Error(res?.msg || res?.message || '数字人主体识别任务提交失败');
+        }
+
+        return { workId };
+      }),
+      tap((res: { workId: string }) => console.log('🤖 数字人主体识别任务已提交:', res.workId)),
+      catchError(this.handleError('identifyDigitalHuman'))
+    );
+  }
+
+  queryDigitalHumanTask(workId: string, routerName: 'getOhIdentifyMain' | 'getOmniHuman'): Observable<any> {
+    const body = {
+      workId,
+      taskId: workId,
+      routerName,
+      token: this.token
+    };
+
+    return this.http.post<any>(`${this.baseUrl}/getOhDateByTask`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      catchError(this.handleError('queryDigitalHumanTask'))
+    );
+  }
+
+  pollDigitalHumanIdentifyUntilReady(
+    workId: string,
+    onProgress?: (status: string, progress: number) => void
+  ): Observable<void> {
+    return new Observable<void>(observer => {
+      let attempt = 0;
+      let finished = false;
+
+      const poll = () => {
+        if (finished) return;
+        attempt += 1;
+
+        if (attempt > this.pollMaxAttempts) {
+          observer.error(new Error(`数字人主体识别超时:已达最大尝试次数 ${this.pollMaxAttempts}`));
+          return;
+        }
+
+        this.queryDigitalHumanTask(workId, 'getOhIdentifyMain').subscribe({
+          next: (res: any) => {
+            if (finished) return;
+
+            const tip = res?.data?.tip || '正在识别主体...';
+            const isContinue = res?.data?.isContinue === true;
+            const progress = Math.min(10 + (attempt / this.pollMaxAttempts) * 90, 99);
+
+            if (onProgress) {
+              onProgress(tip, progress);
+            }
+
+            if (typeof tip === 'string' && tip.includes('不存在主体')) {
+              finished = true;
+              observer.error(new Error('上传图片未识别到可驱动主体,请更换清晰的人像图片'));
+              return;
+            }
+
+            if (isContinue) {
+              finished = true;
+              observer.next();
+              observer.complete();
+              return;
+            }
+
+            setTimeout(poll, this.pollInterval);
+          },
+          error: (err) => {
+            if (!finished && attempt < this.pollMaxAttempts) {
+              setTimeout(poll, this.pollInterval * 2);
+            } else if (!finished) {
+              observer.error(err);
+            }
+          }
+        });
+      };
+
+      poll();
+      return () => { finished = true; };
+    });
+  }
+
+  pollDigitalHumanGenerateUntilComplete(
+    workId: string,
+    resultWorkId: string,
+    onProgress?: (status: string, progress: number) => void
+  ): Observable<{ videoUrl: string; workId: string }> {
+    return new Observable<{ videoUrl: string; workId: string }>(observer => {
+      let attempt = 0;
+      let finished = false;
+      let consecutiveQueryErrors = 0;
+      const QUERY_ERROR_THRESHOLD = 3; // 连续 N 次状态接口失败后回退到结果接口
+
+      const poll = () => {
+        if (finished) return;
+        attempt += 1;
+
+        if (attempt > this.digitalHumanPollMaxAttempts) {
+          this.getDigitalHumanWorkResult(resultWorkId).subscribe((result) => {
+            if (result) {
+              finished = true;
+              observer.next(result);
+              observer.complete();
+              return;
+            }
+
+            observer.error(new Error(`数字人视频生成超时:已达最大尝试次数 ${this.digitalHumanPollMaxAttempts}`));
+          });
+          return;
+        }
+
+        this.queryDigitalHumanTask(workId, 'getOmniHuman').subscribe({
+          next: (res: any) => {
+            if (finished) return;
+            consecutiveQueryErrors = 0;
+
+            const data = res?.data || {};
+            const status = data?.status || '';
+            const isFinish = data?.isFinish === true || status === 'done';
+            const tip = data?.tip || (isFinish ? '数字人视频生成完成' : '正在生成数字人视频...');
+            const progress = Math.min(10 + (attempt / this.digitalHumanPollMaxAttempts) * 90, 99);
+
+            if (onProgress) {
+              onProgress(tip, isFinish ? 100 : progress);
+            }
+
+            if (typeof tip === 'string' && /fail|失败|error|expired|not_found/i.test(tip)) {
+              finished = true;
+              observer.error(new Error(tip));
+              return;
+            }
+
+            if (isFinish) {
+              finished = true;
+              this.getDigitalHumanWorkResult(resultWorkId).subscribe({
+                next: (result) => {
+                  if (!result) {
+                    observer.error(new Error('数字人视频生成完成,但未在作品结果中获取到视频地址'));
+                    return;
+                  }
+
+                  observer.next(result);
+                  observer.complete();
+                },
+                error: (err) => observer.error(err)
+              });
+              return;
+            }
+
+            if (status === 'expired' || status === 'not_found') {
+              finished = true;
+              observer.error(new Error(`数字人任务状态异常:${status}`));
+              return;
+            }
+
+            if (attempt % this.digitalHumanResultCheckInterval === 0) {
+              this.getDigitalHumanWorkResult(resultWorkId).subscribe({
+                next: (result) => {
+                  if (finished) return;
+
+                  if (result) {
+                    finished = true;
+                    if (onProgress) {
+                      onProgress('数字人视频生成完成!', 100);
+                    }
+                    observer.next(result);
+                    observer.complete();
+                    return;
+                  }
+
+                  setTimeout(poll, this.pollInterval);
+                },
+                error: () => {
+                  if (!finished) {
+                    setTimeout(poll, this.pollInterval);
+                  }
+                }
+              });
+              return;
+            }
+
+            setTimeout(poll, this.pollInterval);
+          },
+          error: (err) => {
+            if (finished) return;
+            consecutiveQueryErrors += 1;
+            const status = err?.status;
+
+            // 状态接口连续异常时,回退尝试结果接口,避免被一直 500 卡住
+            if (consecutiveQueryErrors >= QUERY_ERROR_THRESHOLD) {
+              if (consecutiveQueryErrors === QUERY_ERROR_THRESHOLD) {
+                console.warn(`⚠️ 即梦状态接口连续 ${QUERY_ERROR_THRESHOLD} 次异常 (status=${status}),回退到结果接口轮询`);
+              }
+              if (onProgress) {
+                const progress = Math.min(10 + (attempt / this.digitalHumanPollMaxAttempts) * 90, 99);
+                onProgress(`服务端临时异常,正在尝试直接获取结果...(${consecutiveQueryErrors})`, progress);
+              }
+              this.getDigitalHumanWorkResult(resultWorkId).subscribe({
+                next: (result) => {
+                  if (finished) return;
+                  if (result) {
+                    finished = true;
+                    if (onProgress) onProgress('数字人视频生成完成!', 100);
+                    observer.next(result);
+                    observer.complete();
+                    return;
+                  }
+                  if (attempt < this.digitalHumanPollMaxAttempts) {
+                    setTimeout(poll, this.pollInterval * 2);
+                  } else {
+                    observer.error(err);
+                  }
+                },
+                error: () => {
+                  if (finished) return;
+                  if (attempt < this.digitalHumanPollMaxAttempts) {
+                    setTimeout(poll, this.pollInterval * 2);
+                  } else {
+                    observer.error(err);
+                  }
+                }
+              });
+              return;
+            }
+
+            if (attempt < this.digitalHumanPollMaxAttempts) {
+              setTimeout(poll, this.pollInterval * 2);
+            } else {
+              observer.error(err);
+            }
+          }
+        });
+      };
+
+      poll();
+      return () => { finished = true; };
+    });
+  }
+
+  private getDigitalHumanWorkResult(resultWorkId: string): Observable<{ videoUrl: string; workId: string } | null> {
+    return this.getWorkResult(resultWorkId).pipe(
+      map((result: JimengWorkResult) => {
+        const videoUrl = result?.videos?.[0] || '';
+        return videoUrl ? { videoUrl, workId: resultWorkId } : null;
+      }),
+      catchError(() => of(null))
+    );
+  }
+
+  generateDigitalHuman(
+    workId: string,
+    audioUrl: string,
+    options: {
+      prompt?: string;
+      peFastMode?: boolean;
+      outputResolution?: '720p' | '1080p';
+      maskUrl?: string[];
+    } = {},
+    onProgress?: (status: string, progress: number) => void
+  ): Observable<{ videoUrl: string; workId: string }> {
+    const outputResolution = options.outputResolution === '720p' ? 720 : 1080;
+    const body = {
+      workId,
+      audio_url: audioUrl,
+      prompt: options.prompt || '',
+      pe_fast_mode: !!options.peFastMode,
+      output_resolution: outputResolution,
+      mask_url: options.maskUrl || [],
+      token: this.token
+    };
+
+    if (onProgress) onProgress('正在提交数字人生成任务...', 5);
+
+    return this.submitDigitalHumanGenerate(body, onProgress).pipe(
+      switchMap((res: any) => {
+        const generateTaskId = res?.data?.workId || res?.data?.taskId || res?.data?.task_id || res?.workId || res?.taskId || '';
+        const isAccepted = (res?.code === 200 || res?.code === 10000)
+          && (res?.data?.isContinue === true || typeof res?.data?.tip === 'string' || !!generateTaskId);
+
+        if (!isAccepted) {
+          const requestId = res?.request_id ? ` (request_id: ${res.request_id})` : '';
+          const fallback = (() => {
+            try {
+              return JSON.stringify(res);
+            } catch {
+              return '数字人生成任务提交失败';
+            }
+          })();
+          throw new Error(`${res?.msg || res?.message || res?.error || fallback}${requestId}`);
+        }
+
+        if (onProgress) onProgress('数字人任务已提交,正在生成视频...', 15);
+
+        const pollWorkId = generateTaskId || workId;
+
+        return this.pollDigitalHumanGenerateUntilComplete(pollWorkId, workId, (status, progress) => {
+          if (onProgress) onProgress(status || '正在生成数字人视频...', progress);
+        }).pipe(
+          map((result: { videoUrl: string; workId: string }) => {
+            if (onProgress) onProgress('数字人视频生成完成!', 100);
+            return result;
+          })
+        );
+      }),
+      catchError((err) => {
+        if (onProgress) onProgress(`生成失败: ${err.message}`, 0);
+        return throwError(() => err);
+      })
+    );
+  }
+
+  private submitDigitalHumanGenerate(
+    body: any,
+    onProgress?: (status: string, progress: number) => void,
+    attempt: number = 1
+  ): Observable<any> {
+    return this.http.post<any>(`${this.baseUrl}/getOmniHuman`, body, {
+      headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+    }).pipe(
+      tap((res: any) => console.log('🎭 即梦API - 数字人生成提交响应:', res)),
+      catchError((err) => {
+        const retryCode = this.extractJimengErrorCode(err);
+        const shouldRetry = retryCode !== null && this.digitalHumanSubmitRetryableCodes.has(retryCode) && attempt < 3;
+
+        if (!shouldRetry) {
+          return throwError(() => err);
+        }
+
+        const delayMs = attempt * 4000;
+        if (onProgress) {
+          onProgress(`数字人接口繁忙,${Math.round(delayMs / 1000)} 秒后自动重试第 ${attempt + 1} 次...`, 8);
+        }
+
+        return timer(delayMs).pipe(
+          switchMap(() => this.submitDigitalHumanGenerate(body, onProgress, attempt + 1))
+        );
+      })
+    );
+  }
+
+  private extractJimengErrorCode(error: any): number | null {
+    const detail = error?.error;
+    const nested = detail?.message?.errmsg || detail?.errmsg || detail?.error?.errmsg || detail?.message;
+    const code = nested?.code ?? detail?.code ?? error?.status;
+    return typeof code === 'number' ? code : Number.isFinite(Number(code)) ? Number(code) : null;
+  }
+
+  // 轮询任务直到完成,返回最终视频/图片URL
+  pollUntilComplete(
+    workId: string,
+    routerName: string,
+    onProgress?: (status: JimengTaskStatus, attempt: number) => void
+  ): Observable<JimengWorkResult> {
+    return new Observable<JimengWorkResult>(observer => {
+      let attempt = 0;
+      let finished = false;
+
+      const poll = () => {
+        if (finished) return;
+        attempt++;
+
+        if (attempt > this.pollMaxAttempts) {
+          observer.error(new Error(`轮询超时:已达最大尝试次数 ${this.pollMaxAttempts}`));
+          return;
+        }
+
+        this.queryTask(workId, routerName).subscribe({
+          next: (res: any) => {
+            if (finished) return;
+
+            const status: JimengTaskStatus = {
+              tip: res?.data?.tip || '处理中...',
+              isFinish: res?.data?.isFinish === true,
+              isContinue: res?.data?.isContinue === true
+            };
+
+            if (onProgress) {
+              onProgress(status, attempt);
+            }
+
+            console.log(`🔄 轮询 #${attempt}: ${status.tip}, isFinish=${status.isFinish}`);
+
+            if (status.isFinish) {
+              finished = true;
+              // 任务完成,获取最终结果
+              this.getWorkResult(workId).subscribe({
+                next: (result) => {
+                  observer.next(result);
+                  observer.complete();
+                },
+                error: (err) => observer.error(err)
+              });
+            } else {
+              // 继续轮询
+              setTimeout(poll, this.pollInterval);
+            }
+          },
+          error: (err) => {
+            if (!finished) {
+              console.warn(`轮询出错 #${attempt}:`, err);
+              // 出错也继续重试
+              if (attempt < this.pollMaxAttempts) {
+                setTimeout(poll, this.pollInterval * 2);
+              } else {
+                observer.error(err);
+              }
+            }
+          }
+        });
+      };
+
+      poll();
+
+      // 返回清理函数
+      return () => { finished = true; };
+    });
+  }
+
+  // 完整的AI重塑流程:提交任务 → 轮询 → 获取结果
+  remixVideo(
+    prompt: string,
+    options: {
+      method?: '1' | '2';
+      frames?: number;
+      aspectRatio?: string;
+      imageUrl?: string;
+      quality?: '720p' | '1080p' | 'pro';
+    } = {},
+    onProgress?: (status: string, progress: number) => void
+  ): Observable<{ videoUrl: string; workId: string }> {
+    const method = options.method || '1';
+    const quality = options.quality || '1080p';
+
+    const request: JimengVideoRequest = {
+      prompt,
+      method,
+      frames: options.frames || 121,
+      config: method === '1'
+        ? { aspect_ratio: options.aspectRatio || '16:9' }
+        : { image_urls: options.imageUrl ? [options.imageUrl] : [] }
+    };
+
+    if (onProgress) onProgress('正在提交生成任务...', 5);
+
+    const generateFn = quality === 'pro'
+      ? this.generateVideoPro(request)
+      : quality === '720p'
+        ? this.generateVideo720p(request)
+        : this.generateVideo1080p(request);
+
+    return generateFn.pipe(
+      switchMap((res: any) => {
+        if (res?.code !== 200 || !res?.data?.workId) {
+          throw new Error(res?.msg || '任务提交失败');
+        }
+
+        const workId = res.data.workId;
+        console.log(`✅ 任务已提交, workId=${workId}`);
+        if (onProgress) onProgress('任务已提交,正在生成视频...', 15);
+
+        const routerName = quality === 'pro' ? 'getVideoV3_Pro' : quality === '720p' ? 'getVideoV3_720p' : 'getVideoV3_1080p';
+
+        return this.pollUntilComplete(workId, routerName, (status, attempt) => {
+          const progressPercent = Math.min(15 + (attempt / this.pollMaxAttempts) * 75, 90);
+          if (onProgress) onProgress(status.tip || '正在生成视频...', progressPercent);
+        }).pipe(
+          map((result: JimengWorkResult) => {
+            const videoUrl = result.videos?.[0] || '';
+            if (!videoUrl) {
+              throw new Error('未获取到视频URL');
+            }
+            if (onProgress) onProgress('视频生成完成!', 100);
+            return { videoUrl, workId };
+          })
+        );
+      }),
+      catchError((err) => {
+        if (onProgress) onProgress(`生成失败: ${err.message}`, 0);
+        return throwError(() => err);
+      })
+    );
+  }
+
+  // ==================== 一键成片(视频拼接) ====================
+
+  // Quickly 平台凭证
+  private readonly quicklyAppKey = 'ZmNmOGRhNjYzZTAx';
+  private readonly quicklyAppSecret = 'eaa12154c248cad9159a9d6ea8bedf46';
+  private readonly quicklyAccountId = '12859_117409';
+  private readonly quicklyCallbackUrl = 'https://server.fmode.cn/api/functions/cut/onemerge';
+
+  // 生成 Quickly 签名
+  private generateQuicklySign(): { timestamp: string; sign: string } {
+    const timestamp = Date.now().toString();
+    const signStr = timestamp + '#' + this.quicklyAppSecret;
+    const sign = md5(signStr);
+    return { timestamp, sign };
+  }
+
+  // 提交一键成片任务(通过本地后端代理,服务端签名)
+  stitchVideos(
+    videoUrls: string[],
+    options: {
+      tags?: string;
+      proportion?: '9:16' | '3:4' | '1:1';
+      videoDuration?: { min: number; max: number };
+      aiVoice?: number;
+      aiBgm?: number;
+      aiSubtitle?: number;
+    } = {}
+  ): Observable<string> {
+    console.log('🎬 一键成片 - 通过后端代理提交:', { videoUrls: videoUrls.length, options });
+
+    return this.http.post<any>('/backend/api/quickly/create', {
+      videoUrls,
+      options
+    }).pipe(
+      map((res: any) => {
+        console.log('🎬 一键成片 - 响应:', res);
+        const taskId = res?.data?.task_id || res?.task_id;
+        if (!taskId) {
+          throw new Error(res?.message || JSON.stringify(res) || '一键成片任务提交失败');
+        }
+        console.log(`✅ 一键成片任务已提交, taskId=${taskId}`);
+        return taskId;
+      }),
+      catchError(this.handleError('stitchVideos'))
+    );
+  }
+
+  // 查询一键成片结果(通过本地后端代理)
+  queryStitchResult(taskId: string): Observable<any[]> {
+    return this.http.post<any>('/backend/api/quickly/query', { taskId }).pipe(
+      map((res: any) => {
+        if (res?.data && Array.isArray(res.data)) {
+          return res.data;
+        }
+        return [];
+      }),
+      catchError(this.handleError('queryStitchResult'))
+    );
+  }
+
+  // 轮询一键成片结果直到完成
+  pollStitchUntilComplete(
+    taskId: string,
+    onProgress?: (status: string, attempt: number) => void
+  ): Observable<{ videoUrl: string; coverUrl: string; duration: number }> {
+    const STITCH_POLL_INTERVAL = 15000;
+    const STITCH_POLL_MAX = 40;
+
+    return new Observable(observer => {
+      let attempt = 0;
+      let finished = false;
+
+      const poll = () => {
+        if (finished) return;
+        attempt++;
+
+        if (attempt > STITCH_POLL_MAX) {
+          observer.error(new Error(`一键成片轮询超时:已达最大尝试次数 ${STITCH_POLL_MAX}`));
+          return;
+        }
+
+        if (onProgress) onProgress(`正在合成视频... (${attempt}/${STITCH_POLL_MAX})`, attempt);
+
+        this.queryStitchResult(taskId).subscribe({
+          next: (data) => {
+            if (finished) return;
+            console.log(`🔄 一键成片轮询 #${attempt}: ${data.length} 条结果`);
+
+            if (data.length > 0) {
+              finished = true;
+              const result = data[0];
+              observer.next({
+                videoUrl: result.videoUrl,
+                coverUrl: result.coverUrl || '',
+                duration: result.videoDuration || 0
+              });
+              observer.complete();
+            } else {
+              setTimeout(poll, STITCH_POLL_INTERVAL);
+            }
+          },
+          error: (err) => {
+            if (!finished && attempt < STITCH_POLL_MAX) {
+              setTimeout(poll, STITCH_POLL_INTERVAL);
+            } else if (!finished) {
+              observer.error(err);
+            }
+          }
+        });
+      };
+
+      poll();
+      return () => { finished = true; };
+    });
+  }
+
+  private handleError(operation: string) {
+    return (error: any): Observable<never> => {
+      console.error(`即梦API [${operation}] 错误:`, error);
+
+      const rawDetail = error?.error;
+      let detail = rawDetail;
+
+      if (typeof rawDetail === 'string') {
+        try {
+          detail = JSON.parse(rawDetail);
+        } catch {
+          detail = rawDetail;
+        }
+      }
+
+      const nestedErrmsg = typeof detail === 'object'
+        ? detail?.message?.errmsg || detail?.errmsg || detail?.error?.errmsg || null
+        : null;
+
+      const requestIdValue = nestedErrmsg?.request_id || (typeof detail === 'object' ? detail?.request_id : '');
+      const requestId = requestIdValue ? ` (request_id: ${requestIdValue})` : '';
+      const message = typeof detail === 'object'
+        ? nestedErrmsg?.message || detail?.msg || detail?.message?.tip || detail?.message || detail?.error || error?.message
+        : typeof detail === 'string' && detail.trim()
+          ? detail
+          : error?.message || `即梦API ${operation} 调用失败`;
+
+      const fallbackDetail = (() => {
+        if (!rawDetail) return '';
+        if (typeof rawDetail === 'string') return rawDetail;
+        try {
+          return JSON.stringify(rawDetail);
+        } catch {
+          return '';
+        }
+      })();
+
+      const finalMessage = message && !/^Http failure response/i.test(message)
+        ? `${message}${requestId}`
+        : `${message || `即梦API ${operation} 调用失败`}${requestId}${fallbackDetail ? ` | detail: ${fallbackDetail}` : ''}`;
+
+      const normalizedError = new Error(finalMessage);
+      (normalizedError as any).raw = error;
+
+      return throwError(() => normalizedError);
+    };
+  }
+}

+ 225 - 0
src/app/services/llm.service.ts

@@ -0,0 +1,225 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+import { map, catchError } from 'rxjs/operators';
+
+// ==================== 类型定义 ====================
+
+export interface ChatMessage {
+  role: 'system' | 'user' | 'assistant';
+  content: string;
+}
+
+export interface ChatCompletionOptions {
+  model?: string;
+  temperature?: number;
+  max_tokens?: number;
+  stream?: boolean;
+}
+
+export interface ChatCompletionResponse {
+  id: string;
+  model: string;
+  choices: {
+    index: number;
+    message: ChatMessage;
+    finish_reason: string;
+  }[];
+  usage: {
+    prompt_tokens: number;
+    completion_tokens: number;
+    total_tokens: number;
+  };
+}
+
+export interface GeminiPart {
+  text?: string;
+  inlineData?: {
+    mimeType: string;
+    data: string; // base64
+  };
+}
+
+export interface GeminiContent {
+  role?: string;
+  parts: GeminiPart[];
+}
+
+export interface GeminiOptions {
+  model?: string;
+  generationConfig?: {
+    temperature?: number;
+    maxOutputTokens?: number;
+    topP?: number;
+    topK?: number;
+  };
+  systemInstruction?: {
+    parts: { text: string }[];
+  };
+}
+
+export interface GeminiResponse {
+  candidates: {
+    content: {
+      role: string;
+      parts: { text?: string }[];
+    };
+    finishReason: string;
+  }[];
+  usageMetadata: {
+    promptTokenCount: number;
+    candidatesTokenCount: number;
+    totalTokenCount: number;
+  };
+}
+
+// ==================== 服务 ====================
+
+@Injectable({ providedIn: 'root' })
+export class LlmService {
+
+  private readonly chatUrl = '/backend/api/llm/chat';
+  private readonly geminiUrl = '/backend/api/llm/gemini';
+
+  constructor(private http: HttpClient) {}
+
+  // ==================== ChatCompletions ====================
+
+  /**
+   * 发送 ChatCompletions 请求(OpenAI 格式)
+   * 支持 GPT-4o、Gemini、DeepSeek、Claude 等所有模型
+   */
+  chat(messages: ChatMessage[], options: ChatCompletionOptions = {}): Observable<ChatCompletionResponse> {
+    return this.http.post<ChatCompletionResponse>(this.chatUrl, {
+      model: options.model || 'gpt-4o-mini',
+      messages,
+      temperature: options.temperature ?? 0.7,
+      max_tokens: options.max_tokens || 4096,
+      stream: false,
+      ...options
+    }).pipe(
+      catchError(err => {
+        console.error('🤖 LLM Chat 错误:', err);
+        return throwError(() => new Error(err?.error?.error?.message || err?.error?.error || err?.message || 'LLM 请求失败'));
+      })
+    );
+  }
+
+  /**
+   * 快捷方法:发送单条用户消息,返回纯文本
+   */
+  ask(prompt: string, options: ChatCompletionOptions = {}): Observable<string> {
+    return this.chat([{ role: 'user', content: prompt }], options).pipe(
+      map(res => res.choices?.[0]?.message?.content || '')
+    );
+  }
+
+  /**
+   * 快捷方法:带系统提示词发送用户消息,返回纯文本
+   */
+  askWithSystem(systemPrompt: string, userPrompt: string, options: ChatCompletionOptions = {}): Observable<string> {
+    return this.chat([
+      { role: 'system', content: systemPrompt },
+      { role: 'user', content: userPrompt }
+    ], options).pipe(
+      map(res => res.choices?.[0]?.message?.content || '')
+    );
+  }
+
+  // ==================== Gemini 媒体识别 ====================
+
+  /**
+   * 发送 Gemini 原生格式请求(支持图像/视频/音频/PDF 识别)
+   * 媒体通过 base64 inlineData 传入
+   */
+  gemini(contents: GeminiContent[], options: GeminiOptions = {}): Observable<GeminiResponse> {
+    return this.http.post<GeminiResponse>(this.geminiUrl, {
+      model: options.model || 'gemini-2.5-flash',
+      contents,
+      generationConfig: options.generationConfig,
+      systemInstruction: options.systemInstruction
+    }).pipe(
+      catchError(err => {
+        console.error('🤖 Gemini 错误:', err);
+        return throwError(() => new Error(err?.error?.error?.message || err?.error?.error || err?.message || 'Gemini 请求失败'));
+      })
+    );
+  }
+
+  /**
+   * 快捷方法:发送 Gemini 请求,返回纯文本
+   */
+  geminiAsk(contents: GeminiContent[], options: GeminiOptions = {}): Observable<string> {
+    return this.gemini(contents, options).pipe(
+      map(res => res.candidates?.[0]?.content?.parts?.[0]?.text || '')
+    );
+  }
+
+  /**
+   * 图像识别:传入图片 base64 + 提示词
+   */
+  analyzeImage(imageBase64: string, mimeType: string, prompt: string, options: GeminiOptions = {}): Observable<string> {
+    return this.geminiAsk([{
+      parts: [
+        { inlineData: { mimeType, data: imageBase64 } },
+        { text: prompt }
+      ]
+    }], options);
+  }
+
+  /**
+   * 音频识别:传入音频 base64 + 提示词
+   * 支持格式:audio/mp3, audio/wav, audio/aac, audio/ogg, audio/flac
+   */
+  analyzeAudio(audioBase64: string, mimeType: string, prompt: string, options: GeminiOptions = {}): Observable<string> {
+    return this.geminiAsk([{
+      parts: [
+        { inlineData: { mimeType, data: audioBase64 } },
+        { text: prompt }
+      ]
+    }], options);
+  }
+
+  /**
+   * 视频识别:传入视频 base64 + 提示词
+   * 注意:视频文件需小于 20MB(base64 编码后约 27MB)
+   */
+  analyzeVideo(videoBase64: string, mimeType: string, prompt: string, options: GeminiOptions = {}): Observable<string> {
+    return this.geminiAsk([{
+      parts: [
+        { inlineData: { mimeType, data: videoBase64 } },
+        { text: prompt }
+      ]
+    }], options);
+  }
+
+  // ==================== 工具方法 ====================
+
+  /**
+   * 将 File 对象转为 base64 字符串(不含 data:xxx;base64, 前缀)
+   */
+  fileToBase64(file: File): Promise<string> {
+    return new Promise((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onload = () => {
+        const result = reader.result as string;
+        // 去掉 "data:xxx;base64," 前缀
+        const base64 = result.split(',')[1] || result;
+        resolve(base64);
+      };
+      reader.onerror = () => reject(new Error('文件读取失败'));
+      reader.readAsDataURL(file);
+    });
+  }
+
+  /**
+   * 从 URL 加载文件并转为 base64(通过 fetch)
+   */
+  async urlToBase64(url: string): Promise<{ base64: string; mimeType: string }> {
+    const response = await fetch(url);
+    const blob = await response.blob();
+    const mimeType = blob.type || 'application/octet-stream';
+    const base64 = await this.fileToBase64(new File([blob], 'media', { type: mimeType }));
+    return { base64, mimeType };
+  }
+}

+ 58 - 0
src/app/services/workflow.service.ts

@@ -0,0 +1,58 @@
+import { Injectable } from '@angular/core';
+import { Observable, of, throwError } from 'rxjs';
+import { catchError, map, switchMap } from 'rxjs/operators';
+import { DouyinApiService } from './douyin-api.service';
+import { JimengApiService } from './jimeng-api.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class WorkflowService {
+  private tasks = new Map<string, any>();
+
+  constructor(
+    private douyinApi: DouyinApiService,
+    private jimengApi: JimengApiService
+  ) {}
+
+  executeWorkflow(keyword: string): Observable<any> {
+    const taskId = this.generateTaskId();
+    const task = {
+      id: taskId,
+      keyword,
+      status: 'pending',
+      step: 'search',
+      progress: 0,
+      cost: 0,
+      createdAt: new Date(),
+      updatedAt: new Date()
+    };
+
+    this.tasks.set(taskId, task);
+    return of(task);
+  }
+
+  getTaskStatus(taskId: string): any {
+    return this.tasks.get(taskId);
+  }
+
+  getAllTasks(): any[] {
+    return Array.from(this.tasks.values());
+  }
+
+  cancelTask(taskId: string): boolean {
+    const task = this.tasks.get(taskId);
+    if (task) {
+      task.status = 'failed';
+      task.error = '用户取消';
+      return true;
+    }
+    return false;
+  }
+
+  private generateTaskId(): string {
+    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+    const random = Math.random().toString(36).substring(2, 8);
+    return `TASK-${timestamp}-${random}`;
+  }
+}

+ 18 - 0
src/environments/environment.prod.ts

@@ -0,0 +1,18 @@
+export const environment = {
+  production: true,
+  douyinApi: {
+    baseUrl: 'https://api.tikhub.io',
+    token: 'gqsZHfMWgAiMwV+ITbmZy0qALADWBZVS7QnV7kKJe9CwzgWgJG+7bwK+GQ==',
+    timeout: 45000,
+    maxRetries: 2
+  },
+  jimengApi: {
+    baseUrl: 'https://server.fmode.cn/api/volcengine/jimeng',
+    token: 'Bearer r:f0333969e312a40e4703e8fe4ed1c600',
+    parseBaseUrl: 'https://server.fmode.cn/parse',
+    parseAppId: 'ncloudmaster',
+    pollIntervalMs: 4000,
+    pollMaxAttempts: 60,
+    timeout: 60000
+  }
+};

+ 18 - 0
src/environments/environment.ts

@@ -0,0 +1,18 @@
+export const environment = {
+  production: false,
+  douyinApi: {
+    baseUrl: '/api',
+    token: 'gqsZHfMWgAiMwV+ITbmZy0qALADWBZVS7QnV7kKJe9CwzgWgJG+7bwK+GQ==',
+    timeout: 45000,
+    maxRetries: 2
+  },
+  jimengApi: {
+    baseUrl: '/jimeng',
+    token: 'Bearer r:f0333969e312a40e4703e8fe4ed1c600',
+    parseBaseUrl: '/parse',
+    parseAppId: 'ncloudmaster',
+    pollIntervalMs: 4000,
+    pollMaxAttempts: 60,
+    timeout: 60000
+  }
+};

+ 17 - 0
src/index.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>TikTok</title>
+  <base href="/">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <!-- UI/UX Pro Max recommended SaaS typography stack: Inter + JetBrains Mono -->
+  <link rel="preconnect" href="https://fonts.googleapis.com">
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
+</head>
+<body>
+  <app-root></app-root>
+</body>
+</html>

+ 6 - 0
src/main.ts

@@ -0,0 +1,6 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { App } from './app/app';
+
+bootstrapApplication(App, appConfig)
+  .catch((err) => console.error(err));

+ 40 - 0
src/styles.css

@@ -0,0 +1,40 @@
+/* You can add global styles to this file, and also import other style files */
+
+/* 抖音AI视频生成系统 - 全局基础样式
+ * 注意:组件级主题/布局/颜色变量统一定义在 src/app/app.css,
+ * 此文件只保留浏览器级 reset 与字体设置,避免与 app.css 中的 :root / 布局规则冲突。
+ */
+
+html, body {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+  /* UI/UX Pro Max: Inter for SaaS dashboards, with system fallback for CJK */
+  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
+               'Helvetica Neue', Helvetica, Arial,
+               'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+  font-feature-settings: 'cv11', 'ss01', 'ss03';
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-size-adjust: 100%;
+  -webkit-text-size-adjust: 100%;
+}
+
+body {
+  /* 兜底底色(默认日间浅蓝白主题,避免主题加载前闪烁) */
+  background-color: #f1f5fb;
+  color: #0f172a;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+/* 以上为 reset。所有主题/布局/组件样式由 src/app/app.css 定义。
+ * 注意:本文件曾经包含旧版浅色 TikTok 样式(粉/红主题、64px header、.icon 默认显示等),
+ * 与 app.css 的暗紫主题及 52px header 严重冲突,会导致:
+ *   - body 出现整页滚动条(高度差 12px)
+ *   - 侧栏短一截(main-content 高度计算错位)
+ *   - .icon 字符(如 cloud_upload、search)在不该显示处被显示成 Material 字体残片
+ * 故彻底清空,所有规则统一交给 app.css 管理。
+ */

+ 68 - 0
src/video/manifest.json

@@ -0,0 +1,68 @@
+[
+  {
+    "id": "VID-001",
+    "title": "三角洲军机赛远程攻防",
+    "filename": "v0d00fg10000d7906p7og65l2uk2q2v0.MP4",
+    "size": 22230717,
+    "duration": 131,
+    "category": "downloaded",
+    "tags": [
+      "三角洲",
+      "电竞",
+      "远程控制",
+      "搞笑"
+    ],
+    "description": "沉泽三角洲军机赛直播,电脑被远程控制导致的搞笑闹剧",
+    "source": "downloaded",
+    "metadata": {
+      "resolution": "1920x1080",
+      "format": "mp4"
+    },
+    "whisper": {
+      "transcript": "Whisper/TEST-0/v0d00fg10000d7906p7og65l2uk2q2v0.txt",
+      "srt": "Whisper/TEST-0/v0d00fg10000d7906p7og65l2uk2q2v0.srt",
+      "storyboard": "Whisper/TEST-0/storyboard.json",
+      "beautified": "Whisper/TEST-0/beautified_transcript.txt"
+    }
+  },
+  {
+    "id": "VID-002",
+    "title": "抖音视频",
+    "filename": "v2700fgi0000d7d6te7og65nuju3f20g.MP4",
+    "size": 0,
+    "duration": 0,
+    "category": "downloaded",
+    "tags": [],
+    "description": "从抖音下载的视频(待补充)",
+    "source": "downloaded",
+    "metadata": {
+      "resolution": "未知",
+      "format": "mp4"
+    },
+    "whisper": {
+      "transcript": "Whisper/v2700fgi0000d7d6te7og65nuju3f20g/v2700fgi0000d7d6te7og65nuju3f20g.txt",
+      "srt": "Whisper/v2700fgi0000d7d6te7og65nuju3f20g/v2700fgi0000d7d6te7og65nuju3f20g.srt",
+      "segments": "Whisper/v2700fgi0000d7d6te7og65nuju3f20g/v2700fgi0000d7d6te7og65nuju3f20g.json"
+    }
+  },
+  {
+    "id": "VID-1775972859088",
+    "title": "v0200fg10000d77thuvog65n8n8anl4g",
+    "filename": "v0200fg10000d77thuvog65n8n8anl4g.MP4",
+    "size": 9560312,
+    "duration": 0,
+    "category": "uploaded",
+    "tags": [],
+    "description": "用户上传的视频",
+    "source": "uploaded",
+    "metadata": {
+      "resolution": "未知",
+      "format": "mp4"
+    },
+    "whisper": {
+      "transcript": "Whisper/v0200fg10000d77thuvog65n8n8anl4g/v0200fg10000d77thuvog65n8n8anl4g.txt",
+      "srt": "Whisper/v0200fg10000d77thuvog65n8n8anl4g/v0200fg10000d77thuvog65n8n8anl4g.srt",
+      "segments": "Whisper/v0200fg10000d77thuvog65n8n8anl4g/v0200fg10000d77thuvog65n8n8anl4g.json"
+    }
+  }
+]

+ 28 - 0
stream.txt

@@ -0,0 +1,28 @@
+data: {"choices":[{"delta":{"content":"","refusal":null,"role":"assistant"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"1vAR71EWj","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":"P"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"LaJdOgDemQ","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":"ong"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"n254dAVZ","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":"!"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"FcJcLFBhqb","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":" How"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"rN8mUZu","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":" can"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"97Udsw2","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":" I"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"H1u9o3Zr9","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":" assist"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"F61F","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":" you"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"ZgXlFvR","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":" today"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"PgHFN","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{"content":"?"},"finish_reason":null,"index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"LEkEeSfJrU","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[{"delta":{},"finish_reason":"stop","index":0,"logprobs":null}],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"gepa8","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":null}
+
+data: {"choices":[],"created":1777349299,"id":"chatcmpl-DZU277HmcIwY5Ph4uTXrwiXoLHV6M","model":"gpt-4o-mini-2024-07-18","obfuscation":"WqMgzJd18NK","object":"chat.completion.chunk","service_tier":"default","system_fingerprint":"fp_eb37e061ec","usage":{"completion_tokens":11,"completion_tokens_details":{"accepted_prediction_tokens":0,"audio_tokens":0,"reasoning_tokens":0,"rejected_prediction_tokens":0},"prompt_tokens":8,"prompt_tokens_details":{"audio_tokens":0,"cached_tokens":0},"total_tokens":19}}
+
+data: [DONE]
+

+ 15 - 0
tsconfig.app.json

@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./out-tsc/app",
+    "types": []
+  },
+  "include": [
+    "src/**/*.ts"
+  ],
+  "exclude": [
+    "src/**/*.spec.ts"
+  ]
+}

+ 33 - 0
tsconfig.json

@@ -0,0 +1,33 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+  "compileOnSave": false,
+  "compilerOptions": {
+    "strict": true,
+    "noImplicitOverride": true,
+    "noPropertyAccessFromIndexSignature": true,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "skipLibCheck": true,
+    "isolatedModules": true,
+    "experimentalDecorators": true,
+    "importHelpers": true,
+    "target": "ES2022",
+    "module": "preserve"
+  },
+  "angularCompilerOptions": {
+    "enableI18nLegacyMessageIdFormat": false,
+    "strictInjectionParameters": true,
+    "strictInputAccessModifiers": true,
+    "strictTemplates": true
+  },
+  "files": [],
+  "references": [
+    {
+      "path": "./tsconfig.app.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}

+ 15 - 0
tsconfig.spec.json

@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "./out-tsc/spec",
+    "types": [
+      "vitest/globals"
+    ]
+  },
+  "include": [
+    "src/**/*.d.ts",
+    "src/**/*.spec.ts"
+  ]
+}