111
This commit is contained in:
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -5112,7 +5112,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-app"
|
name = "tauri-app"
|
||||||
version = "0.1.9"
|
version = "0.1.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "tauri-app"
|
name = "tauri-app"
|
||||||
version = "0.1.9"
|
version = "0.1.11"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const APP_COMMANDS: &[&str] = &[
|
|||||||
"run_nodejs_script",
|
"run_nodejs_script",
|
||||||
"run_nodejs_script_source",
|
"run_nodejs_script_source",
|
||||||
"list_nodejs_scripts",
|
"list_nodejs_scripts",
|
||||||
|
"run_python_script",
|
||||||
|
"run_python_script_source",
|
||||||
|
"list_python_scripts",
|
||||||
|
"stop_python_script",
|
||||||
"read_local_file_base64",
|
"read_local_file_base64",
|
||||||
"write_local_file_base64",
|
"write_local_file_base64",
|
||||||
"get_media_duration_seconds",
|
"get_media_duration_seconds",
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ permissions = [
|
|||||||
"allow-run-nodejs-script",
|
"allow-run-nodejs-script",
|
||||||
"allow-run-nodejs-script-source",
|
"allow-run-nodejs-script-source",
|
||||||
"allow-list-nodejs-scripts",
|
"allow-list-nodejs-scripts",
|
||||||
|
"allow-run-python-script",
|
||||||
|
"allow-run-python-script-source",
|
||||||
|
"allow-list-python-scripts",
|
||||||
|
"allow-stop-python-script",
|
||||||
"allow-read-local-file-base64",
|
"allow-read-local-file-base64",
|
||||||
"allow-write-local-file-base64",
|
"allow-write-local-file-base64",
|
||||||
"allow-get-media-duration-seconds",
|
"allow-get-media-duration-seconds",
|
||||||
|
|||||||
@@ -693,12 +693,13 @@ class AdvancedCoverGenerator:
|
|||||||
print(f"Converted text position from string to dict: {text_position}", file=sys.stderr)
|
print(f"Converted text position from string to dict: {text_position}", file=sys.stderr)
|
||||||
|
|
||||||
# 加载字体
|
# 加载字体
|
||||||
font_path = self._get_font_path(font_family)
|
from modules.chinese_font import load_cjk_font
|
||||||
|
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
font = load_cjk_font(font_family, font_size, font_weight)
|
||||||
except:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to load font {font_path}, using default", file=sys.stderr)
|
print(f"Warning: Failed to load title font: {e}", file=sys.stderr)
|
||||||
font = ImageFont.load_default()
|
font = load_cjk_font("SimHei", font_size, font_weight)
|
||||||
|
|
||||||
# ✅ 关键修复:支持多行文字,使用anchor='mm'实现中心对齐
|
# ✅ 关键修复:支持多行文字,使用anchor='mm'实现中心对齐
|
||||||
# 计算文字中心点位置(百分比转像素)
|
# 计算文字中心点位置(百分比转像素)
|
||||||
@@ -949,8 +950,8 @@ class AdvancedCoverGenerator:
|
|||||||
font_size = font_size_from_config
|
font_size = font_size_from_config
|
||||||
# 重新加载字体
|
# 重新加载字体
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
font = load_cjk_font(font_family, font_size, font_weight)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 获取排版参数 - 优先使用 titles.main 中的值,否则使用顶级配置,最后使用默认值
|
# 获取排版参数 - 优先使用 titles.main 中的值,否则使用顶级配置,最后使用默认值
|
||||||
@@ -1391,12 +1392,15 @@ class AdvancedCoverGenerator:
|
|||||||
print(f"Converted subtitle position from string to dict: {subtitle_position}", file=sys.stderr)
|
print(f"Converted subtitle position from string to dict: {subtitle_position}", file=sys.stderr)
|
||||||
|
|
||||||
# 加载副标题字体
|
# 加载副标题字体
|
||||||
subtitle_font_path = self._get_font_path(subtitle_font_family)
|
from modules.chinese_font import load_cjk_font
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subtitle_font = ImageFont.truetype(subtitle_font_path, subtitle_font_size)
|
subtitle_font = load_cjk_font(
|
||||||
except:
|
subtitle_font_family, subtitle_font_size, subtitle_font_weight
|
||||||
print(f"Warning: Failed to load subtitle font {subtitle_font_path}, using default", file=sys.stderr)
|
)
|
||||||
subtitle_font = ImageFont.load_default()
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to load subtitle font: {e}", file=sys.stderr)
|
||||||
|
subtitle_font = load_cjk_font("SimHei", subtitle_font_size, subtitle_font_weight)
|
||||||
|
|
||||||
# 计算副标题中心点位置(百分比转像素)
|
# 计算副标题中心点位置(百分比转像素)
|
||||||
# ✅ 修复旋转坐标问题:有旋转时Y轴需要反转,无旋转时不需要
|
# ✅ 修复旋转坐标问题:有旋转时Y轴需要反转,无旋转时不需要
|
||||||
@@ -1442,8 +1446,14 @@ class AdvancedCoverGenerator:
|
|||||||
subtitle_font_size = subtitle_font_size_from_config
|
subtitle_font_size = subtitle_font_size_from_config
|
||||||
# 重新加载字体
|
# 重新加载字体
|
||||||
try:
|
try:
|
||||||
subtitle_font = ImageFont.truetype(subtitle_font_path, subtitle_font_size)
|
from modules.chinese_font import load_cjk_font
|
||||||
except:
|
|
||||||
|
subtitle_font = load_cjk_font(
|
||||||
|
subtitle_font_family,
|
||||||
|
subtitle_font_size,
|
||||||
|
subtitle_font_weight,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 获取排版参数 - 优先使用 titles.sub 中的值,否则使用顶级配置,最后使用默认值
|
# 获取排版参数 - 优先使用 titles.sub 中的值,否则使用顶级配置,最后使用默认值
|
||||||
@@ -1937,13 +1947,13 @@ class AdvancedCoverGenerator:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 如果还是找不到,使用默认字体
|
# 如果还是找不到,使用默认字体
|
||||||
if not font_path or not os.path.exists(font_path):
|
try:
|
||||||
print(f"Warning: Font file not found for '{font_family}', using fallback", file=sys.stderr)
|
from modules.chinese_font import resolve_cjk_font_path
|
||||||
if font_path:
|
|
||||||
print(f" Attempted path: {font_path}", file=sys.stderr)
|
|
||||||
return 'C:\\Windows\\Fonts\\msyh.ttc'
|
|
||||||
|
|
||||||
return font_path
|
return resolve_cjk_font_path(font_family, 400)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: _get_font_path fallback: {e}", file=sys.stderr)
|
||||||
|
return str(Path(os.environ.get("WINDIR", "C:/Windows")) / "Fonts" / "simhei.ttf")
|
||||||
|
|
||||||
# ✅ 标准封面尺寸 3:4 比例(与前端 CoverCustomEditor.vue 一致)
|
# ✅ 标准封面尺寸 3:4 比例(与前端 CoverCustomEditor.vue 一致)
|
||||||
COVER_WIDTH = 1080
|
COVER_WIDTH = 1080
|
||||||
|
|||||||
@@ -115,11 +115,16 @@ def add_text_to_image(img, title, config):
|
|||||||
|
|
||||||
style = config.get('style', 'default') # default, outline, blur_bg, gradient, split
|
style = config.get('style', 'default') # default, outline, blur_bg, gradient, split
|
||||||
|
|
||||||
# 加载字体
|
# 加载字体(禁止 load_default,否则中文乱码)
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype(font_path, font_size)
|
from modules.chinese_font import load_cjk_font
|
||||||
except:
|
|
||||||
font = ImageFont.load_default()
|
font = load_cjk_font(font_family, font_size)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[cover_generator] font load failed: {e}, retry SimHei", file=sys.stderr)
|
||||||
|
from modules.chinese_font import load_cjk_font
|
||||||
|
|
||||||
|
font = load_cjk_font("SimHei", font_size)
|
||||||
|
|
||||||
# 创建绘图对象
|
# 创建绘图对象
|
||||||
draw = ImageDraw.Draw(pil_img)
|
draw = ImageDraw.Draw(pil_img)
|
||||||
|
|||||||
161
src-tauri/resources/cover-python/modules/chinese_font.py
Normal file
161
src-tauri/resources/cover-python/modules/chinese_font.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""封面中文 PIL / ffmpeg 字体解析(避免 load_default 导致乱码)"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import ImageFont
|
||||||
|
except ImportError:
|
||||||
|
ImageFont = None # type: ignore
|
||||||
|
|
||||||
|
_FAMILY_ALIASES = {
|
||||||
|
"microsoft yahei": "Microsoft YaHei",
|
||||||
|
"微软雅黑": "Microsoft YaHei",
|
||||||
|
"simhei": "SimHei",
|
||||||
|
"黑体": "SimHei",
|
||||||
|
"simsun": "SimSun",
|
||||||
|
"宋体": "SimSun",
|
||||||
|
"arial": "Arial",
|
||||||
|
"times new roman": "Times New Roman",
|
||||||
|
}
|
||||||
|
|
||||||
|
_WIN_FAMILY_FILES = {
|
||||||
|
"SimHei": ["simhei.ttf"],
|
||||||
|
"Microsoft YaHei": ["msyh.ttc", "msyh.ttf", "msyhbd.ttc"],
|
||||||
|
"SimSun": ["simsun.ttc", "simsunb.ttf"],
|
||||||
|
"Arial": ["arial.ttf", "arialbd.ttf"],
|
||||||
|
"Times New Roman": ["times.ttf", "timesbd.ttf"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _scripts_dir() -> Optional[Path]:
|
||||||
|
env = os.environ.get("AICLIENT_COVER_SCRIPTS_DIR", "").strip()
|
||||||
|
if env:
|
||||||
|
p = Path(env)
|
||||||
|
if p.is_dir():
|
||||||
|
return p
|
||||||
|
# advanced_cover_generator.py 位于 scripts/cover/
|
||||||
|
here = Path(__file__).resolve().parent.parent
|
||||||
|
if (here / "advanced_cover_generator.py").is_file():
|
||||||
|
return here
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _windows_fonts_dir() -> Path:
|
||||||
|
windir = os.environ.get("WINDIR", "C:\\Windows")
|
||||||
|
return Path(windir) / "Fonts"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_family(font_family: Optional[str]) -> str:
|
||||||
|
raw = (font_family or "Microsoft YaHei").strip()
|
||||||
|
key = raw.lower()
|
||||||
|
return _FAMILY_ALIASES.get(key, raw)
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_filenames(family: str, weight: int = 400) -> List[str]:
|
||||||
|
names = list(_WIN_FAMILY_FILES.get(family, []))
|
||||||
|
if weight >= 700 and family == "Microsoft YaHei":
|
||||||
|
names = ["msyhbd.ttc", "msyhl.ttc"] + names
|
||||||
|
# 通用回退:.ttf 优先于 .ttc(PIL 对 ttf 更稳)
|
||||||
|
fallback = ["simhei.ttf", "msyh.ttc", "simsun.ttc", "msyh.ttf"]
|
||||||
|
out: List[str] = []
|
||||||
|
for n in names + fallback:
|
||||||
|
if n not in out:
|
||||||
|
out.append(n)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_cjk_font_path(
|
||||||
|
font_family: Optional[str] = None,
|
||||||
|
weight: int = 400,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
解析可用于 PIL / ffmpeg 的中文字体文件路径。
|
||||||
|
"""
|
||||||
|
family = _normalize_family(font_family)
|
||||||
|
candidates = _candidate_filenames(family, weight)
|
||||||
|
|
||||||
|
scripts = _scripts_dir()
|
||||||
|
if scripts:
|
||||||
|
fonts_dir = scripts / "fonts"
|
||||||
|
if fonts_dir.is_dir():
|
||||||
|
for name in candidates:
|
||||||
|
p = fonts_dir / name
|
||||||
|
if p.is_file():
|
||||||
|
return str(p.resolve())
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
win_fonts = _windows_fonts_dir()
|
||||||
|
for name in candidates:
|
||||||
|
p = win_fonts / name
|
||||||
|
if p.is_file():
|
||||||
|
return str(p.resolve())
|
||||||
|
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
mac_candidates = [
|
||||||
|
"/System/Library/Fonts/PingFang.ttc",
|
||||||
|
"/System/Library/Fonts/STHeiti Light.ttc",
|
||||||
|
"/Library/Fonts/Arial Unicode.ttf",
|
||||||
|
]
|
||||||
|
for p in mac_candidates:
|
||||||
|
if os.path.isfile(p):
|
||||||
|
return p
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
linux_candidates = [
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||||
|
"/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",
|
||||||
|
]
|
||||||
|
for p in linux_candidates:
|
||||||
|
if os.path.isfile(p):
|
||||||
|
return p
|
||||||
|
|
||||||
|
# 最后尝试:Windows 黑体
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
simhei = _windows_fonts_dir() / "simhei.ttf"
|
||||||
|
if simhei.is_file():
|
||||||
|
return str(simhei.resolve())
|
||||||
|
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"未找到中文字体(family={family}),请在 cover/fonts 放置 simhei.ttf 或安装系统字体"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_cjk_font(
|
||||||
|
font_family: Optional[str],
|
||||||
|
size: int,
|
||||||
|
weight: int = 400,
|
||||||
|
) -> "ImageFont.FreeTypeFont":
|
||||||
|
"""加载支持中文的 PIL 字体,禁止回退到 load_default。"""
|
||||||
|
if ImageFont is None:
|
||||||
|
raise RuntimeError("PIL ImageFont 不可用")
|
||||||
|
|
||||||
|
path = resolve_cjk_font_path(font_family, weight)
|
||||||
|
ext = os.path.splitext(path)[1].lower()
|
||||||
|
|
||||||
|
if ext == ".ttc":
|
||||||
|
last_err: Optional[Exception] = None
|
||||||
|
for index in range(8):
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(path, size, index=index)
|
||||||
|
print(
|
||||||
|
f"[chinese_font] Loaded TTC index={index}: {path} size={size}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return font
|
||||||
|
except OSError as e:
|
||||||
|
last_err = e
|
||||||
|
continue
|
||||||
|
raise OSError(f"无法加载字体集合 {path}: {last_err}")
|
||||||
|
|
||||||
|
font = ImageFont.truetype(path, size)
|
||||||
|
print(f"[chinese_font] Loaded: {path} size={size}", file=sys.stderr)
|
||||||
|
return font
|
||||||
@@ -8,6 +8,7 @@ pub mod sound_asr;
|
|||||||
pub mod video;
|
pub mod video;
|
||||||
pub mod fs_util;
|
pub mod fs_util;
|
||||||
pub mod nodejs;
|
pub mod nodejs;
|
||||||
|
pub mod pythonscript;
|
||||||
pub mod publish;
|
pub mod publish;
|
||||||
pub mod quickjs;
|
pub mod quickjs;
|
||||||
pub mod software_info;
|
pub mod software_info;
|
||||||
|
|||||||
214
src-tauri/src/commands/pythonscript.rs
Normal file
214
src-tauri/src/commands/pythonscript.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
//! Python 流水线命令:封面等脚本由后端 `/api/v1/python-scripts` 下发,
|
||||||
|
//! 由打包的 `python.exe` 执行。
|
||||||
|
//!
|
||||||
|
//! 前端用法:
|
||||||
|
//! ```ignore
|
||||||
|
//! import { invoke } from "@tauri-apps/api/core";
|
||||||
|
//! import { listen } from "@tauri-apps/api/event";
|
||||||
|
//!
|
||||||
|
//! const off = await listen("python:event", (e) => console.log(e.payload));
|
||||||
|
//! const result = await invoke("run_python_script", {
|
||||||
|
//! scriptName: "cover_generate.py",
|
||||||
|
//! params: { videoPath, titleText, effectStyle },
|
||||||
|
//! });
|
||||||
|
//! off();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tauri::{AppHandle, Emitter, Window};
|
||||||
|
use tokio::sync::{mpsc, watch, Mutex};
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::js_runtime::PipelineEvent;
|
||||||
|
use crate::pythonscript;
|
||||||
|
|
||||||
|
const EVENT_NAME: &str = "python:event";
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PythonTaskRegistry {
|
||||||
|
inner: Mutex<HashMap<String, watch::Sender<bool>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
#[serde(tag = "type", rename_all = "camelCase")]
|
||||||
|
pub enum FrontendEvent {
|
||||||
|
Progress {
|
||||||
|
#[serde(rename = "scriptName")]
|
||||||
|
script_name: String,
|
||||||
|
status: String,
|
||||||
|
},
|
||||||
|
Log {
|
||||||
|
#[serde(rename = "scriptName")]
|
||||||
|
script_name: String,
|
||||||
|
level: String,
|
||||||
|
msg: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
fields: Option<Value>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_python_event(script_name: &str, level: &str, msg: &str, fields: &Option<Value>) {
|
||||||
|
let extra = fields
|
||||||
|
.as_ref()
|
||||||
|
.map(|f| format!(" {f}"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let line = format!("[{script_name}] {msg}{extra}");
|
||||||
|
match level {
|
||||||
|
"error" => log::error!(target: "pythonscript", "{line}"),
|
||||||
|
"warn" => log::warn!(target: "pythonscript", "{line}"),
|
||||||
|
_ => log::info!(target: "pythonscript", "{line}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_event_pump(
|
||||||
|
app: AppHandle,
|
||||||
|
window: Window,
|
||||||
|
script_name: String,
|
||||||
|
mut rx: mpsc::UnboundedReceiver<PipelineEvent>,
|
||||||
|
) -> tokio::task::JoinHandle<()> {
|
||||||
|
let win_label = window.label().to_string();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(ev) = rx.recv().await {
|
||||||
|
let payload = match &ev {
|
||||||
|
PipelineEvent::Progress(s) => FrontendEvent::Progress {
|
||||||
|
script_name: script_name.clone(),
|
||||||
|
status: s.clone(),
|
||||||
|
},
|
||||||
|
PipelineEvent::Log {
|
||||||
|
level,
|
||||||
|
msg,
|
||||||
|
fields,
|
||||||
|
} => {
|
||||||
|
log_python_event(&script_name, level, msg, fields);
|
||||||
|
FrontendEvent::Log {
|
||||||
|
script_name: script_name.clone(),
|
||||||
|
level: level.clone(),
|
||||||
|
msg: msg.clone(),
|
||||||
|
fields: fields.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let _ = app.emit_to(&win_label, EVENT_NAME, payload);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finish_event_pump(mut pump: tokio::task::JoinHandle<()>) {
|
||||||
|
if timeout(Duration::from_millis(300), &mut pump).await.is_err() {
|
||||||
|
pump.abort();
|
||||||
|
let _ = pump.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 按脚本名运行:从后端拉取脚本及本地 import 依赖,spawn python 子进程。
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn run_python_script(
|
||||||
|
app: AppHandle,
|
||||||
|
window: Window,
|
||||||
|
app_config: tauri::State<'_, AppConfig>,
|
||||||
|
registry: tauri::State<'_, PythonTaskRegistry>,
|
||||||
|
script_name: String,
|
||||||
|
params: Value,
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
let (cancel_tx, cancel_rx) = watch::channel(false);
|
||||||
|
{
|
||||||
|
let mut map = registry.inner.lock().await;
|
||||||
|
if map.contains_key(&script_name) {
|
||||||
|
return Err(format!("脚本正在运行: {script_name}"));
|
||||||
|
}
|
||||||
|
map.insert(script_name.clone(), cancel_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||||
|
let pump = spawn_event_pump(app, window, script_name.clone(), rx);
|
||||||
|
let config_env = app_config.env_for_node().await;
|
||||||
|
|
||||||
|
let result = pythonscript::run_python_script(
|
||||||
|
script_name.clone(),
|
||||||
|
params,
|
||||||
|
tx,
|
||||||
|
config_env,
|
||||||
|
cancel_rx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut map = registry.inner.lock().await;
|
||||||
|
map.remove(&script_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = result?;
|
||||||
|
|
||||||
|
finish_event_pump(pump).await;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 调试:直接执行传入的 Python 源码(仍从服务端拉取 import 依赖)。
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn run_python_script_source(
|
||||||
|
app: AppHandle,
|
||||||
|
window: Window,
|
||||||
|
app_config: tauri::State<'_, AppConfig>,
|
||||||
|
registry: tauri::State<'_, PythonTaskRegistry>,
|
||||||
|
script_source: String,
|
||||||
|
params: Value,
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
let script_key = "<debug-python-source>".to_string();
|
||||||
|
let (cancel_tx, cancel_rx) = watch::channel(false);
|
||||||
|
{
|
||||||
|
let mut map = registry.inner.lock().await;
|
||||||
|
if map.contains_key(&script_key) {
|
||||||
|
return Err("调试 Python 脚本正在运行".to_string());
|
||||||
|
}
|
||||||
|
map.insert(script_key.clone(), cancel_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel::<PipelineEvent>();
|
||||||
|
let pump = spawn_event_pump(app, window, script_key.clone(), rx);
|
||||||
|
let config_env = app_config.env_for_node().await;
|
||||||
|
|
||||||
|
let result = pythonscript::run_python_script_source(
|
||||||
|
script_source,
|
||||||
|
params,
|
||||||
|
tx,
|
||||||
|
config_env,
|
||||||
|
cancel_rx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut map = registry.inner.lock().await;
|
||||||
|
map.remove(&script_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = result?;
|
||||||
|
|
||||||
|
finish_event_pump(pump).await;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_python_scripts() -> Result<Vec<String>, String> {
|
||||||
|
pythonscript::list_scripts().await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn stop_python_script(
|
||||||
|
registry: tauri::State<'_, PythonTaskRegistry>,
|
||||||
|
script_name: String,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let sender = {
|
||||||
|
let map = registry.inner.lock().await;
|
||||||
|
map.get(&script_name).cloned()
|
||||||
|
};
|
||||||
|
if let Some(tx) = sender {
|
||||||
|
tx.send(true).map_err(|e| e.to_string())?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
//! 封面 Python 运行时与脚本目录定位(对齐 Electron bundled python-runtime)
|
//! 封面 Python 运行时定位(bundled `resources/pythonruntime/python.exe`)
|
||||||
|
//! 封面脚本由服务端下发,不在此目录打包。
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
fn exe_name() -> &'static str {
|
fn exe_name() -> &'static str {
|
||||||
@@ -10,7 +12,43 @@ fn exe_name() -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// bundled `python-runtimebackup` 或系统 / 环境变量 Python。
|
fn pythonruntime_candidates() -> Vec<PathBuf> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
// 开发源码树优先:target/debug/resources 可能是残缺副本(缺 PIL 等)
|
||||||
|
out.push(
|
||||||
|
PathBuf::from("src-tauri/resources/pythonruntime").join(exe_name()),
|
||||||
|
);
|
||||||
|
out.push(PathBuf::from("resources/pythonruntime").join(exe_name()));
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if let Some(dir) = exe.parent() {
|
||||||
|
out.push(
|
||||||
|
dir.join("resources")
|
||||||
|
.join("pythonruntime")
|
||||||
|
.join(exe_name()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `python.exe` 所在目录即 PYTHONHOME(含 Lib/site-packages)。
|
||||||
|
pub fn python_home(python_exe: &Path) -> PathBuf {
|
||||||
|
python_exe
|
||||||
|
.parent()
|
||||||
|
.map(|p| p.to_path_buf())
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn site_packages_dir(python_exe: &Path) -> PathBuf {
|
||||||
|
python_home(python_exe).join("Lib").join("site-packages")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_has_pillow(python_exe: &Path) -> bool {
|
||||||
|
let site = site_packages_dir(python_exe);
|
||||||
|
site.join("PIL").is_dir() || site.join("Pillow").is_dir()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// bundled `resources/pythonruntime/python.exe` 或 `AICLIENT_PYTHON_PATH`。
|
||||||
pub fn locate_python() -> Option<PathBuf> {
|
pub fn locate_python() -> Option<PathBuf> {
|
||||||
if let Ok(p) = std::env::var("AICLIENT_PYTHON_PATH") {
|
if let Ok(p) = std::env::var("AICLIENT_PYTHON_PATH") {
|
||||||
let pb = PathBuf::from(&p);
|
let pb = PathBuf::from(&p);
|
||||||
@@ -18,35 +56,46 @@ pub fn locate_python() -> Option<PathBuf> {
|
|||||||
return Some(pb);
|
return Some(pb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
for path in pythonruntime_candidates() {
|
||||||
if let Some(dir) = exe.parent() {
|
if path.is_file() && runtime_has_pillow(&path) {
|
||||||
let bundled = dir
|
return Some(path);
|
||||||
.join("resources")
|
|
||||||
.join("resources-bundles")
|
|
||||||
.join("python-runtimebackup")
|
|
||||||
.join(exe_name());
|
|
||||||
if bundled.exists() {
|
|
||||||
return Some(bundled);
|
|
||||||
}
|
|
||||||
let cover_only = dir.join("resources").join("cover-python").join(exe_name());
|
|
||||||
if cover_only.exists() {
|
|
||||||
return Some(cover_only);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dev_bundled = PathBuf::from("src-tauri/resources/resources-bundles/python-runtimebackup")
|
for path in pythonruntime_candidates() {
|
||||||
.join(exe_name());
|
if path.is_file() {
|
||||||
if dev_bundled.exists() {
|
return Some(path);
|
||||||
return Some(dev_bundled);
|
}
|
||||||
}
|
|
||||||
let dev_cover = PathBuf::from("src-tauri/resources/cover-python").join(exe_name());
|
|
||||||
if dev_cover.exists() {
|
|
||||||
return Some(dev_cover);
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 封面脚本目录(`advanced_cover_generator.py` 等)。
|
/// 为 bundled Python 子进程设置 PYTHONHOME / PYTHONPATH(否则找不到 PIL/cv2)。
|
||||||
|
pub fn apply_python_runtime_env(
|
||||||
|
env: &mut HashMap<String, String>,
|
||||||
|
python_exe: &Path,
|
||||||
|
bundle_dir: Option<&Path>,
|
||||||
|
) {
|
||||||
|
let home = python_home(python_exe);
|
||||||
|
env.insert(
|
||||||
|
"PYTHONHOME".into(),
|
||||||
|
home.to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
let site = site_packages_dir(python_exe);
|
||||||
|
let sep = if cfg!(windows) { ";" } else { ":" };
|
||||||
|
let mut paths = vec![site.to_string_lossy().into_owned()];
|
||||||
|
if let Some(dir) = bundle_dir {
|
||||||
|
paths.push(dir.to_string_lossy().into_owned());
|
||||||
|
}
|
||||||
|
env.insert("PYTHONPATH".into(), paths.join(sep));
|
||||||
|
env.remove("PYTHONUSERBASE");
|
||||||
|
env.insert("PYTHONNOUSERSITE".into(), "1".into());
|
||||||
|
env.insert(
|
||||||
|
"AICLIENT_PYTHON_HOME".into(),
|
||||||
|
home.to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 开发期本地脚本目录(发布构建由服务端拉取,不依赖此路径)。
|
||||||
pub fn locate_cover_scripts_dir() -> Option<PathBuf> {
|
pub fn locate_cover_scripts_dir() -> Option<PathBuf> {
|
||||||
if let Ok(p) = std::env::var("AICLIENT_COVER_SCRIPTS_DIR") {
|
if let Ok(p) = std::env::var("AICLIENT_COVER_SCRIPTS_DIR") {
|
||||||
let pb = PathBuf::from(&p);
|
let pb = PathBuf::from(&p);
|
||||||
@@ -54,18 +103,6 @@ pub fn locate_cover_scripts_dir() -> Option<PathBuf> {
|
|||||||
return Some(pb);
|
return Some(pb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
|
||||||
if let Some(dir) = exe.parent() {
|
|
||||||
let bundled = dir.join("resources").join("cover-python");
|
|
||||||
if bundled.join("advanced_cover_generator.py").is_file() {
|
|
||||||
return Some(bundled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let dev = PathBuf::from("src-tauri/resources/cover-python");
|
|
||||||
if dev.join("advanced_cover_generator.py").is_file() {
|
|
||||||
return Some(dev);
|
|
||||||
}
|
|
||||||
let backend = PathBuf::from("../pythonbackend/scripts/cover");
|
let backend = PathBuf::from("../pythonbackend/scripts/cover");
|
||||||
if backend.join("advanced_cover_generator.py").is_file() {
|
if backend.join("advanced_cover_generator.py").is_file() {
|
||||||
return backend.canonicalize().ok();
|
return backend.canonicalize().ok();
|
||||||
@@ -89,10 +126,10 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cover_scripts_dev_path_exists() {
|
fn pythonruntime_dev_path() {
|
||||||
let dev = Path::new("src-tauri/resources/cover-python/advanced_cover_generator.py");
|
let dev = PathBuf::from("src-tauri/resources/pythonruntime/python.exe");
|
||||||
if dev.is_file() {
|
if dev.is_file() {
|
||||||
assert!(locate_cover_scripts_dir().is_some());
|
assert!(locate_python().is_some());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub mod ffmpeg;
|
|||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod js_runtime;
|
pub mod js_runtime;
|
||||||
pub mod nodejs;
|
pub mod nodejs;
|
||||||
|
pub mod pythonscript;
|
||||||
pub mod agent_id;
|
pub mod agent_id;
|
||||||
pub mod oem_context;
|
pub mod oem_context;
|
||||||
pub mod oem_id;
|
pub mod oem_id;
|
||||||
@@ -60,6 +61,7 @@ pub fn run() {
|
|||||||
.manage(video_store::VideoStore::new())
|
.manage(video_store::VideoStore::new())
|
||||||
.manage(publish_store::PublishStore::new())
|
.manage(publish_store::PublishStore::new())
|
||||||
.manage(commands::nodejs::NodeTaskRegistry::default())
|
.manage(commands::nodejs::NodeTaskRegistry::default())
|
||||||
|
.manage(commands::pythonscript::PythonTaskRegistry::default())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
@@ -131,6 +133,12 @@ pub fn run() {
|
|||||||
commands::nodejs::run_nodejs_script_source,
|
commands::nodejs::run_nodejs_script_source,
|
||||||
commands::nodejs::list_nodejs_scripts,
|
commands::nodejs::list_nodejs_scripts,
|
||||||
commands::nodejs::stop_nodejs_script,
|
commands::nodejs::stop_nodejs_script,
|
||||||
|
commands::pythonscript::run_python_script,
|
||||||
|
commands::pythonscript::run_python_script_source,
|
||||||
|
commands::pythonscript::list_python_scripts,
|
||||||
|
commands::pythonscript::stop_python_script,
|
||||||
|
|
||||||
|
|
||||||
commands::fs_util::read_local_file_base64,
|
commands::fs_util::read_local_file_base64,
|
||||||
commands::fs_util::write_local_file_base64,
|
commands::fs_util::write_local_file_base64,
|
||||||
commands::fs_util::get_media_duration_seconds,
|
commands::fs_util::get_media_duration_seconds,
|
||||||
|
|||||||
591
src-tauri/src/pythonscript.rs
Normal file
591
src-tauri/src/pythonscript.rs
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
//! Bundled Python runner:脚本由后端 `GET /api/v1/python-scripts?name=<path>.py` 下发,
|
||||||
|
//! 在 bundled `python.exe` 中执行,协议与 Node 路径对齐(`__EVT__` / `__RESULT__` / `__python_main`)。
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde_json::Value;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use tokio::sync::watch;
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::cover_python;
|
||||||
|
use crate::js_runtime::PipelineEvent;
|
||||||
|
|
||||||
|
static RE_SAFE_SCRIPT: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9._/-]*\.py$").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
static RE_FROM_IMPORT: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"(?m)^\s*from\s+([\w.]+)\s+import").unwrap());
|
||||||
|
|
||||||
|
static RE_IMPORT: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"(?m)^\s*import\s+([\w.]+)").unwrap());
|
||||||
|
|
||||||
|
static RE_FROM_REL: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"(?m)^\s*from\s+\.([\w]+)\s+import").unwrap());
|
||||||
|
|
||||||
|
const INLINE_ENTRY: &str = "__inline_entry__.py";
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn hide_console_window(cmd: &mut Command) {
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||||
|
cmd.as_std_mut().creation_flags(CREATE_NO_WINDOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn hide_console_window(_cmd: &mut Command) {}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PythonError {
|
||||||
|
#[error("python.exe 未找到(请检查 src-tauri/resources/pythonruntime/python.exe)")]
|
||||||
|
NotFound,
|
||||||
|
#[error("io: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error("拉取脚本失败: {0}")]
|
||||||
|
FetchScript(String),
|
||||||
|
#[error("python 进程非零退出 status={status:?}, stderr={stderr}")]
|
||||||
|
NonZero { status: Option<i32>, stderr: String },
|
||||||
|
#[error("脚本未输出 __RESULT__ 行")]
|
||||||
|
NoResult,
|
||||||
|
#[error("json: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
#[error("任务已取消")]
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_script_name(name: &str) -> Option<String> {
|
||||||
|
let raw = name.trim().replace('\\', "/");
|
||||||
|
if raw.contains("..") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if RE_SAFE_SCRIPT.is_match(&raw) {
|
||||||
|
Some(raw)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn module_to_rel_path(module: &str) -> Option<String> {
|
||||||
|
if module.starts_with("modules.") {
|
||||||
|
return Some(format!("{}.py", module.replace('.', "/")));
|
||||||
|
}
|
||||||
|
if module == "modules" {
|
||||||
|
return Some("modules/__init__.py".into());
|
||||||
|
}
|
||||||
|
match module {
|
||||||
|
"advanced_cover_generator" | "cover_generator" | "cover_generate" => {
|
||||||
|
Some(format!("{}.py", module))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relative_import_to_path(current_rel: &str, sub: &str) -> Option<String> {
|
||||||
|
let parent = Path::new(current_rel).parent().unwrap_or(Path::new(""));
|
||||||
|
let joined = parent.join(format!("{}.py", sub));
|
||||||
|
let s = joined.to_string_lossy().replace('\\', "/");
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_local_deps(source: &str, current_rel: &str) -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for cap in RE_FROM_IMPORT.captures_iter(source) {
|
||||||
|
if let Some(m) = cap.get(1) {
|
||||||
|
if let Some(p) = module_to_rel_path(m.as_str()) {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for cap in RE_IMPORT.captures_iter(source) {
|
||||||
|
if let Some(m) = cap.get(1) {
|
||||||
|
if let Some(p) = module_to_rel_path(m.as_str()) {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for cap in RE_FROM_REL.captures_iter(source) {
|
||||||
|
if let Some(sub) = cap.get(1) {
|
||||||
|
if let Some(p) = relative_import_to_path(current_rel, sub.as_str()) {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_python_script_source(script_name: &str) -> Result<String, PythonError> {
|
||||||
|
let q = urlencoding::encode(script_name);
|
||||||
|
let path = format!("/python-scripts?name={q}");
|
||||||
|
let (_status, v) = crate::api_client::request_json("GET", &path, None, HashMap::new())
|
||||||
|
.await
|
||||||
|
.map_err(PythonError::FetchScript)?;
|
||||||
|
if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) {
|
||||||
|
let msg = v
|
||||||
|
.get("message")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("未知错误");
|
||||||
|
return Err(PythonError::FetchScript(msg.to_string()));
|
||||||
|
}
|
||||||
|
let source = v
|
||||||
|
.get("data")
|
||||||
|
.and_then(|d| d.get("source"))
|
||||||
|
.and_then(|s| s.as_str())
|
||||||
|
.ok_or_else(|| PythonError::FetchScript("响应缺少 data.source".into()))?;
|
||||||
|
Ok(source.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_python_script_source_optional(
|
||||||
|
script_name: &str,
|
||||||
|
required: bool,
|
||||||
|
) -> Result<Option<String>, PythonError> {
|
||||||
|
match fetch_python_script_source(script_name).await {
|
||||||
|
Ok(s) => Ok(Some(s)),
|
||||||
|
Err(PythonError::FetchScript(msg))
|
||||||
|
if !required && (msg.contains("不存在") || msg.contains("无效的脚本")) =>
|
||||||
|
{
|
||||||
|
log::warn!(
|
||||||
|
target: "pythonscript",
|
||||||
|
"skip missing optional script dependency: {script_name} ({msg})"
|
||||||
|
);
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_script_bundle(entry: &str) -> Result<HashMap<String, String>, PythonError> {
|
||||||
|
let entry_owned = normalize_script_name(entry)
|
||||||
|
.ok_or_else(|| PythonError::FetchScript(format!("无效的入口脚本名: {entry}")))?;
|
||||||
|
|
||||||
|
let mut modules: HashMap<String, String> = HashMap::new();
|
||||||
|
let mut stack = vec![entry_owned.clone()];
|
||||||
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
while let Some(name) = stack.pop() {
|
||||||
|
if !seen.insert(name.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if modules.contains_key(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let required = name == entry_owned;
|
||||||
|
let Some(source) = fetch_python_script_source_optional(&name, required).await? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for dep in extract_local_deps(&source, &name) {
|
||||||
|
if !seen.contains(&dep) {
|
||||||
|
stack.push(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modules.insert(name, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_script_bundle_for_inline(
|
||||||
|
inline_source: &str,
|
||||||
|
) -> Result<HashMap<String, String>, PythonError> {
|
||||||
|
let mut modules: HashMap<String, String> = HashMap::new();
|
||||||
|
let mut stack: Vec<String> = extract_local_deps(inline_source, INLINE_ENTRY)
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
let mut seen: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
while let Some(name) = stack.pop() {
|
||||||
|
if !seen.insert(name.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if modules.contains_key(&name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(source) = fetch_python_script_source_optional(&name, false).await? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for dep in extract_local_deps(&source, &name) {
|
||||||
|
if !seen.contains(&dep) {
|
||||||
|
stack.push(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modules.insert(name, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.insert(INLINE_ENTRY.to_string(), inline_source.to_string());
|
||||||
|
Ok(modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_module_name(entry_rel: &str) -> String {
|
||||||
|
Path::new(entry_rel)
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("user_entry")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_file_runner(entry_rel: &str) -> String {
|
||||||
|
let entry_file = entry_rel.replace('\\', "/");
|
||||||
|
let entry_mod = entry_module_name(entry_rel);
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
# ---- aiclient auto-generated python runner (do not edit) ----
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def _emit(obj):
|
||||||
|
sys.stderr.write("__EVT__" + json.dumps(obj, ensure_ascii=False) + "\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
class _Native:
|
||||||
|
@staticmethod
|
||||||
|
def emit_progress(status):
|
||||||
|
_emit({{"type": "progress", "status": str(status)}})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def log(level, msg, fields=None):
|
||||||
|
_emit({{
|
||||||
|
"type": "log",
|
||||||
|
"level": str(level or "info"),
|
||||||
|
"msg": str(msg),
|
||||||
|
"fields": fields,
|
||||||
|
}})
|
||||||
|
|
||||||
|
def main():
|
||||||
|
raw = sys.stdin.read()
|
||||||
|
params = json.loads(raw) if raw.strip() else None
|
||||||
|
bundle_dir = os.getcwd()
|
||||||
|
if bundle_dir not in sys.path:
|
||||||
|
sys.path.insert(0, bundle_dir)
|
||||||
|
entry_file = os.path.join(bundle_dir, "{entry_file}")
|
||||||
|
entry_mod = "{entry_mod}"
|
||||||
|
spec = importlib.util.spec_from_file_location(entry_mod, entry_file)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError("无法加载入口脚本: " + entry_file)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
mod.__dict__["__native"] = _Native
|
||||||
|
sys.modules[entry_mod] = mod
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
fn = getattr(mod, "__python_main", None)
|
||||||
|
if fn is None:
|
||||||
|
raise RuntimeError("脚本未定义 __python_main(params)")
|
||||||
|
result = fn(params)
|
||||||
|
sys.stdout.write("__RESULT__" + json.dumps(result, ensure_ascii=False) + "\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
_emit({{"type": "log", "level": "error", "msg": traceback.format_exc()}})
|
||||||
|
sys.exit(2)
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_script_bundle(
|
||||||
|
modules: &HashMap<String, String>,
|
||||||
|
entry: &str,
|
||||||
|
) -> Result<(PathBuf, PathBuf), PythonError> {
|
||||||
|
let tmp_dir = std::env::temp_dir().join("aiclient-python");
|
||||||
|
tokio::fs::create_dir_all(&tmp_dir).await?;
|
||||||
|
let bundle_dir = tmp_dir.join(format!("bundle-{}", Uuid::new_v4()));
|
||||||
|
tokio::fs::create_dir_all(&bundle_dir).await?;
|
||||||
|
|
||||||
|
for (name, source) in modules {
|
||||||
|
let rel = name.replace('\\', "/");
|
||||||
|
let dest = bundle_dir.join(&rel);
|
||||||
|
if let Some(parent) = dest.parent() {
|
||||||
|
tokio::fs::create_dir_all(parent).await?;
|
||||||
|
}
|
||||||
|
tokio::fs::write(&dest, source).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let runner_path = bundle_dir.join("_runner.py");
|
||||||
|
tokio::fs::write(&runner_path, build_file_runner(entry)).await?;
|
||||||
|
|
||||||
|
Ok((bundle_dir, runner_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_scripts() -> Result<Vec<String>, PythonError> {
|
||||||
|
let (_status, v) =
|
||||||
|
crate::api_client::request_json("GET", "/python-scripts/list", None, HashMap::new())
|
||||||
|
.await
|
||||||
|
.map_err(PythonError::FetchScript)?;
|
||||||
|
if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) {
|
||||||
|
let msg = v
|
||||||
|
.get("message")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("未知错误");
|
||||||
|
return Err(PythonError::FetchScript(msg.to_string()));
|
||||||
|
}
|
||||||
|
let names = v
|
||||||
|
.get("data")
|
||||||
|
.and_then(|d| d.get("names"))
|
||||||
|
.and_then(|n| n.as_array())
|
||||||
|
.map(|arr| {
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|x| x.as_str().map(|s| s.to_string()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_python_script(
|
||||||
|
script_name: String,
|
||||||
|
params: Value,
|
||||||
|
events: UnboundedSender<PipelineEvent>,
|
||||||
|
config_env: HashMap<String, String>,
|
||||||
|
mut cancel_rx: watch::Receiver<bool>,
|
||||||
|
) -> Result<Value, PythonError> {
|
||||||
|
let entry = normalize_script_name(&script_name)
|
||||||
|
.ok_or_else(|| PythonError::FetchScript(format!("无效的脚本名称: {script_name}")))?;
|
||||||
|
let modules = resolve_script_bundle(&entry).await?;
|
||||||
|
run_script_bundle(&entry, modules, params, events, config_env, &mut cancel_rx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_python_script_source(
|
||||||
|
source: String,
|
||||||
|
params: Value,
|
||||||
|
events: UnboundedSender<PipelineEvent>,
|
||||||
|
config_env: HashMap<String, String>,
|
||||||
|
mut cancel_rx: watch::Receiver<bool>,
|
||||||
|
) -> Result<Value, PythonError> {
|
||||||
|
let modules = resolve_script_bundle_for_inline(&source).await?;
|
||||||
|
run_script_bundle(
|
||||||
|
INLINE_ENTRY,
|
||||||
|
modules,
|
||||||
|
params,
|
||||||
|
events,
|
||||||
|
config_env,
|
||||||
|
&mut cancel_rx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_script_bundle(
|
||||||
|
entry: &str,
|
||||||
|
modules: HashMap<String, String>,
|
||||||
|
params: Value,
|
||||||
|
events: UnboundedSender<PipelineEvent>,
|
||||||
|
config_env: HashMap<String, String>,
|
||||||
|
cancel_rx: &mut watch::Receiver<bool>,
|
||||||
|
) -> Result<Value, PythonError> {
|
||||||
|
let python = cover_python::locate_python().ok_or(PythonError::NotFound)?;
|
||||||
|
|
||||||
|
let (bundle_dir, script_path) = write_script_bundle(&modules, entry).await?;
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&python);
|
||||||
|
cmd.arg("-u")
|
||||||
|
.arg(&script_path)
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.current_dir(&bundle_dir)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
let mut env_map: HashMap<String, String> = std::env::vars().collect();
|
||||||
|
env_map.insert("PYTHONIOENCODING".into(), "utf-8".into());
|
||||||
|
env_map.insert("PYTHONUTF8".into(), "1".into());
|
||||||
|
cover_python::apply_python_runtime_env(&mut env_map, &python, Some(&bundle_dir));
|
||||||
|
for (k, v) in config_env {
|
||||||
|
env_map.insert(k, v);
|
||||||
|
}
|
||||||
|
if let Some(ffmpeg) = crate::ffmpeg::locate_ffmpeg() {
|
||||||
|
if ffmpeg.exists() {
|
||||||
|
env_map.insert(
|
||||||
|
"AICLIENT_FFMPEG_PATH".into(),
|
||||||
|
ffmpeg.to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
let path_key = "PATH";
|
||||||
|
let new_path = cover_python::prepend_ffmpeg_to_path(
|
||||||
|
env_map.get(path_key).map(|s| s.as_str()).unwrap_or(""),
|
||||||
|
);
|
||||||
|
env_map.insert(path_key.into(), new_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env_map.insert(
|
||||||
|
"AICLIENT_COVER_SCRIPTS_DIR".into(),
|
||||||
|
bundle_dir.to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
env_map.insert(
|
||||||
|
"AICLIENT_PYTHON_PATH".into(),
|
||||||
|
python.to_string_lossy().into_owned(),
|
||||||
|
);
|
||||||
|
for (k, v) in env_map {
|
||||||
|
cmd.env(k, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
hide_console_window(&mut cmd);
|
||||||
|
let mut child = cmd.spawn()?;
|
||||||
|
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
let json = serde_json::to_vec(¶ms)?;
|
||||||
|
stdin.write_all(&json).await?;
|
||||||
|
stdin.shutdown().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stderr = child.stderr.take().expect("stderr piped");
|
||||||
|
let events_tx = events.clone();
|
||||||
|
let mut stderr_task = tokio::spawn(async move {
|
||||||
|
let mut reader = BufReader::new(stderr).lines();
|
||||||
|
let mut tail = String::new();
|
||||||
|
while let Ok(Some(line)) = reader.next_line().await {
|
||||||
|
if let Some(rest) = line.strip_prefix("__EVT__") {
|
||||||
|
if let Ok(v) = serde_json::from_str::<Value>(rest) {
|
||||||
|
let typ = v.get("type").and_then(|x| x.as_str()).unwrap_or("");
|
||||||
|
match typ {
|
||||||
|
"progress" => {
|
||||||
|
let s = v
|
||||||
|
.get("status")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let _ = events_tx.send(PipelineEvent::Progress(s));
|
||||||
|
}
|
||||||
|
"log" => {
|
||||||
|
let level = v
|
||||||
|
.get("level")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("info")
|
||||||
|
.to_string();
|
||||||
|
let msg = v
|
||||||
|
.get("msg")
|
||||||
|
.and_then(|x| x.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let fields = v.get("fields").cloned().filter(|x| !x.is_null());
|
||||||
|
let _ = events_tx.send(PipelineEvent::Log { level, msg, fields });
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let _ = events_tx.send(PipelineEvent::Log {
|
||||||
|
level: "info".into(),
|
||||||
|
msg: rest.to_string(),
|
||||||
|
fields: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !line.is_empty() {
|
||||||
|
let _ = events_tx.send(PipelineEvent::Log {
|
||||||
|
level: "warn".into(),
|
||||||
|
msg: line.clone(),
|
||||||
|
fields: None,
|
||||||
|
});
|
||||||
|
tail.push_str(&line);
|
||||||
|
tail.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tail
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = child.stdout.take().expect("stdout piped");
|
||||||
|
let events_tx2 = events.clone();
|
||||||
|
let mut stdout_task = tokio::spawn(async move {
|
||||||
|
let mut reader = BufReader::new(stdout).lines();
|
||||||
|
while let Ok(Some(line)) = reader.next_line().await {
|
||||||
|
if let Some(rest) = line.strip_prefix("__RESULT__") {
|
||||||
|
return Some(rest.to_string());
|
||||||
|
} else if !line.is_empty() {
|
||||||
|
let _ = events_tx2.send(PipelineEvent::Log {
|
||||||
|
level: "info".into(),
|
||||||
|
msg: line,
|
||||||
|
fields: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
|
||||||
|
let (status, cancelled, result_line, stdout_done) = tokio::select! {
|
||||||
|
result = &mut stdout_task => {
|
||||||
|
let result_line = result.unwrap_or(None);
|
||||||
|
let status = match timeout(Duration::from_secs(120), child.wait()).await {
|
||||||
|
Ok(status) => status?,
|
||||||
|
Err(_) => {
|
||||||
|
let _ = child.start_kill();
|
||||||
|
child.wait().await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(status, false, result_line, true)
|
||||||
|
}
|
||||||
|
status = child.wait() => (status?, false, None, false),
|
||||||
|
_ = wait_for_cancel(cancel_rx) => {
|
||||||
|
let _ = child.start_kill();
|
||||||
|
let status = child.wait().await?;
|
||||||
|
(status, true, None, false)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stderr_tail = match timeout(Duration::from_millis(500), &mut stderr_task).await {
|
||||||
|
Ok(joined) => joined.unwrap_or_default(),
|
||||||
|
Err(_) => {
|
||||||
|
stderr_task.abort();
|
||||||
|
let _ = stderr_task.await;
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result_line_fallback = if stdout_done {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
match timeout(Duration::from_millis(500), &mut stdout_task).await {
|
||||||
|
Ok(joined) => joined.unwrap_or(None),
|
||||||
|
Err(_) => {
|
||||||
|
stdout_task.abort();
|
||||||
|
let _ = stdout_task.await;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result_line = result_line.or(result_line_fallback);
|
||||||
|
|
||||||
|
let _ = tokio::fs::remove_dir_all(&bundle_dir).await;
|
||||||
|
|
||||||
|
if cancelled {
|
||||||
|
return Err(PythonError::Cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result_str) = result_line {
|
||||||
|
if !status.success() {
|
||||||
|
log::warn!(
|
||||||
|
target: "pythonscript",
|
||||||
|
"python exited non-zero after __RESULT__: status={:?}, stderr_tail={}",
|
||||||
|
status.code(),
|
||||||
|
stderr_tail
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let v: Value = serde_json::from_str(&result_str)?;
|
||||||
|
return Ok(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(PythonError::NonZero {
|
||||||
|
status: status.code(),
|
||||||
|
stderr: stderr_tail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(PythonError::NoResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_cancel(cancel_rx: &mut watch::Receiver<bool>) {
|
||||||
|
if *cancel_rx.borrow() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while cancel_rx.changed().await.is_ok() {
|
||||||
|
if *cancel_rx.borrow() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "aiclient",
|
"productName": "aiclient",
|
||||||
"version": "0.1.9",
|
"version": "0.1.11",
|
||||||
"identifier": "com.aiclient.desktop",
|
"identifier": "com.aiclient.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev --prefix ../aiclient_ui",
|
"beforeDevCommand": "npm run dev --prefix ../aiclient_ui",
|
||||||
|
|||||||
176
templates/cover-template-人像居中.json
Normal file
176
templates/cover-template-人像居中.json
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:13:49.997Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "v08ezr6v3x-mkdite6i",
|
||||||
|
"name": "人像居中",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": true,
|
||||||
|
"backgroundBlurIntensity": 30,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 54.99999999999999,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 53.935185185185205,
|
||||||
|
"y": 34.375
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": true,
|
||||||
|
"personBorderStyle": "solid",
|
||||||
|
"personBorderColor": "#FFFFFF",
|
||||||
|
"personBorderWidth": 8,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 6,
|
||||||
|
"subtitleMaxLength": 12,
|
||||||
|
"titleText": "托德揭秘芒格",
|
||||||
|
"titleFontFamily": "墨趣古风体",
|
||||||
|
"titleFontSize": 110,
|
||||||
|
"titleFontWeight": 100,
|
||||||
|
"titleColor": "#ffffff",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 0,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435502861.9124,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 12,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.0488,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 16,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.4207,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 14,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 39.814814814814795,
|
||||||
|
"y": 69.40972222222223
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "horizontal",
|
||||||
|
"titleCharSpacing": 12,
|
||||||
|
"titleLineSpacing": 118,
|
||||||
|
"titleMaxCharsPerLine": 10,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "rectangle",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "投资稳的原因",
|
||||||
|
"subtitleFontFamily": "庞门正道标题体免费版",
|
||||||
|
"subtitleFontSize": 117,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#eaf273",
|
||||||
|
"subtitleStrokeColor": "#000000",
|
||||||
|
"subtitleStrokeWidth": 0,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435591571.2886,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 2,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"opacity": 60,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.856,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 0,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 65,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.3809,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 4,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 50,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 58.796296296296305,
|
||||||
|
"y": 77.84722222222221
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "horizontal",
|
||||||
|
"subtitleCharSpacing": 12,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 15,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
164
templates/cover-template-大四字报.json
Normal file
164
templates/cover-template-大四字报.json
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:13:38.566Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "705fa010-5bf3-4041-b624-0fd364bea5ad",
|
||||||
|
"name": "大四字报",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": true,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": false,
|
||||||
|
"backgroundBlurIntensity": 30,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 33.125,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 22.68518518518519,
|
||||||
|
"y": 78.125
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": false,
|
||||||
|
"personBorderStyle": "solid",
|
||||||
|
"personBorderColor": "#FFFFFF",
|
||||||
|
"personBorderWidth": 8,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 30,
|
||||||
|
"titleText": "礼貌回归",
|
||||||
|
"titleFontFamily": "USMCCyuanjiantecu",
|
||||||
|
"titleFontSize": 375,
|
||||||
|
"titleFontWeight": 400,
|
||||||
|
"titleColor": "#e6ca19",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 0,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765355385133.3418,
|
||||||
|
"offsetX": 20,
|
||||||
|
"offsetY": 20,
|
||||||
|
"blur": 30,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765355385133.7036,
|
||||||
|
"offsetX": -20,
|
||||||
|
"offsetY": -20,
|
||||||
|
"blur": 30,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 52.54629629629629,
|
||||||
|
"y": 48.40277777777778
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "horizontal",
|
||||||
|
"titleCharSpacing": 58,
|
||||||
|
"titleLineSpacing": 150,
|
||||||
|
"titleMaxCharsPerLine": 2,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "polygon",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 95.83333333333333,
|
||||||
|
"y": 68.88888888888889
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [
|
||||||
|
{
|
||||||
|
"x": -330.24691358024694,
|
||||||
|
"y": -15.440538194444414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 40,
|
||||||
|
"y": -40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 40,
|
||||||
|
"y": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -40,
|
||||||
|
"y": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -114.9691358024691,
|
||||||
|
"y": 36.642795138888864
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -96.45061728395058,
|
||||||
|
"y": -3.2877604166666865
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "行动亲爱的观众朋友们",
|
||||||
|
"subtitleFontFamily": "USMCCyuanjiantecu",
|
||||||
|
"subtitleFontSize": 122,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#e37a16",
|
||||||
|
"subtitleStrokeColor": "#000000",
|
||||||
|
"subtitleStrokeWidth": 5,
|
||||||
|
"subtitleShadowColor": "#ff0000",
|
||||||
|
"subtitleShadowOffsetX": 41,
|
||||||
|
"subtitleShadowOffsetY": 50,
|
||||||
|
"subtitleShadowBlur": 13,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 51.15740740740741,
|
||||||
|
"y": 83.75
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "horizontal",
|
||||||
|
"subtitleCharSpacing": 0,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 8,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
templates/cover-template-斜黄白.json
Normal file
137
templates/cover-template-斜黄白.json
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:12:07.312Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "8f17c467-c69c-45b2-8011-94a6c3071d16",
|
||||||
|
"name": "斜黄白",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": false,
|
||||||
|
"backgroundBlurIntensity": 10,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 80,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": false,
|
||||||
|
"personBorderStyle": "solid",
|
||||||
|
"personBorderColor": "#FFFFFF",
|
||||||
|
"personBorderWidth": 8,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 10,
|
||||||
|
"titleText": "礼貌回归",
|
||||||
|
"titleFontFamily": "斗鱼追光体2.0",
|
||||||
|
"titleFontSize": 120,
|
||||||
|
"titleFontWeight": 700,
|
||||||
|
"titleColor": "#f5d400",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 3,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 42.59259259259258,
|
||||||
|
"y": 56.909722222222214
|
||||||
|
},
|
||||||
|
"titleRotation": 338,
|
||||||
|
"titleDirection": "horizontal",
|
||||||
|
"titleCharSpacing": 24,
|
||||||
|
"titleLineSpacing": 144,
|
||||||
|
"titleMaxCharsPerLine": 10,
|
||||||
|
"titleBackgroundEnabled": true,
|
||||||
|
"titleBackgroundColor": "#f5f5f5",
|
||||||
|
"titleBackgroundOpacity": 25,
|
||||||
|
"titleBackgroundShape": "polygon",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 44.21296296296296,
|
||||||
|
"y": 32.951388888888886
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [
|
||||||
|
{
|
||||||
|
"x": 226.08024691358025,
|
||||||
|
"y": -79.67664930555557
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 250.7716049382716,
|
||||||
|
"y": 218.93446180555554
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -260.8024691358025,
|
||||||
|
"y": 691.1566840277777
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": -290.12345679012344,
|
||||||
|
"y": 366.50390624999994
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "行动亲爱的观众朋友们",
|
||||||
|
"subtitleFontFamily": "斗鱼追光体2.0",
|
||||||
|
"subtitleFontSize": 82,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#FFFFFF",
|
||||||
|
"subtitleStrokeColor": "#000000",
|
||||||
|
"subtitleStrokeWidth": 1,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 45.37037037037037,
|
||||||
|
"y": 66.5625
|
||||||
|
},
|
||||||
|
"subtitleRotation": 338,
|
||||||
|
"subtitleDirection": "horizontal",
|
||||||
|
"subtitleCharSpacing": 23,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 17,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
templates/cover-template-粉红虚白.json
Normal file
148
templates/cover-template-粉红虚白.json
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:13:56.933Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "43t1uykj8zg-mkdite6i",
|
||||||
|
"name": "粉红虚白",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": true,
|
||||||
|
"backgroundBlurIntensity": 50,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 81.7361111111111,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 49.768518518518526,
|
||||||
|
"y": 60.41666666666666
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": true,
|
||||||
|
"personBorderStyle": "dashed",
|
||||||
|
"personBorderColor": "#ffffff",
|
||||||
|
"personBorderWidth": 8,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 11,
|
||||||
|
"titleText": "长期持有",
|
||||||
|
"titleFontFamily": "庞门正道标题体免费版",
|
||||||
|
"titleFontSize": 165,
|
||||||
|
"titleFontWeight": 100,
|
||||||
|
"titleColor": "#e8c4df",
|
||||||
|
"titleStrokeColor": "#e147c0",
|
||||||
|
"titleStrokeWidth": 6,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 36.80555555555553,
|
||||||
|
"y": 11.944444444444452
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "horizontal",
|
||||||
|
"titleCharSpacing": 11,
|
||||||
|
"titleLineSpacing": 118,
|
||||||
|
"titleMaxCharsPerLine": 10,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "rectangle",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "才是真省心又赚钱",
|
||||||
|
"subtitleFontFamily": "方正粗黑宋简体",
|
||||||
|
"subtitleFontSize": 117,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#FFFFFF",
|
||||||
|
"subtitleStrokeColor": "#ef62e3",
|
||||||
|
"subtitleStrokeWidth": 5,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435591571.2886,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 2,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"opacity": 60,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.856,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 0,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 65,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.3809,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 4,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 50,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 51.3888888888889,
|
||||||
|
"y": 23.333333333333304
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "horizontal",
|
||||||
|
"subtitleCharSpacing": 12,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 15,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
templates/cover-template-红黄虚描.json
Normal file
176
templates/cover-template-红黄虚描.json
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:14:00.925Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "sb8mm7l4bjf-mkdite6i",
|
||||||
|
"name": "红黄虚描",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": true,
|
||||||
|
"backgroundBlurIntensity": 60,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 65.06944444444444,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 62.73148148148148,
|
||||||
|
"y": 67.0138888888889
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": true,
|
||||||
|
"personBorderStyle": "dashed",
|
||||||
|
"personBorderColor": "#FFFFFF",
|
||||||
|
"personBorderWidth": 12,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 10,
|
||||||
|
"titleText": "托德揭秘",
|
||||||
|
"titleFontFamily": "泼墨体",
|
||||||
|
"titleFontSize": 165,
|
||||||
|
"titleFontWeight": 100,
|
||||||
|
"titleColor": "#d4ec74",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 0,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435502861.9124,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 12,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.0488,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 16,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.4207,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 14,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 55.09259259259257,
|
||||||
|
"y": 15.59027777777779
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "horizontal",
|
||||||
|
"titleCharSpacing": 19,
|
||||||
|
"titleLineSpacing": 118,
|
||||||
|
"titleMaxCharsPerLine": 9,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "rectangle",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "芒格投资稳的原因",
|
||||||
|
"subtitleFontFamily": "MaokenAssortedSans-Lite",
|
||||||
|
"subtitleFontSize": 117,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#7e1010",
|
||||||
|
"subtitleStrokeColor": "#ffffff",
|
||||||
|
"subtitleStrokeWidth": 6,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435591571.2886,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 2,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"opacity": 60,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.856,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 0,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 65,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.3809,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 4,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 50,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 18.98148148148148,
|
||||||
|
"y": 64.47916666666667
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "vertical",
|
||||||
|
"subtitleCharSpacing": 12,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 15,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
templates/cover-template-蓝底白字.json
Normal file
139
templates/cover-template-蓝底白字.json
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:13:43.982Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "6404718a-1b1c-4148-9807-ad3c57a53e0c",
|
||||||
|
"name": "蓝底白字",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": false,
|
||||||
|
"backgroundBlurIntensity": 10,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 80,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": false,
|
||||||
|
"personBorderStyle": "solid",
|
||||||
|
"personBorderColor": "#FFFFFF",
|
||||||
|
"personBorderWidth": 8,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 5,
|
||||||
|
"titleText": "礼貌回归",
|
||||||
|
"titleFontFamily": "USMCCyuanjiantecu",
|
||||||
|
"titleFontSize": 120,
|
||||||
|
"titleFontWeight": 700,
|
||||||
|
"titleColor": "#cbcdb1",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 0,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765449844664.8303,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 8,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 80,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765449844664.415,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 16,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 40,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 50.694444444444436,
|
||||||
|
"y": 63.506944444444436
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "horizontal",
|
||||||
|
"titleCharSpacing": 24,
|
||||||
|
"titleLineSpacing": 144,
|
||||||
|
"titleMaxCharsPerLine": 10,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "rectangle",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "行动亲爱的",
|
||||||
|
"subtitleFontFamily": "千图厚黑体",
|
||||||
|
"subtitleFontSize": 187,
|
||||||
|
"subtitleFontWeight": 100,
|
||||||
|
"subtitleColor": "#FFFFFF",
|
||||||
|
"subtitleStrokeColor": "#000000",
|
||||||
|
"subtitleStrokeWidth": 10,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 49.53703703703704,
|
||||||
|
"y": 74.20138888888889
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "horizontal",
|
||||||
|
"subtitleCharSpacing": -23,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 15,
|
||||||
|
"subtitleBackgroundEnabled": true,
|
||||||
|
"subtitleBackgroundColor": "#599dd9",
|
||||||
|
"subtitleBackgroundOpacity": 25,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 161.0185185185185,
|
||||||
|
"height": 30.48611111111111
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50.23148148148148,
|
||||||
|
"y": 67.11805555555556
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
templates/cover-template-青蓝虚描.json
Normal file
167
templates/cover-template-青蓝虚描.json
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:13:52.918Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "1xwrrcvf7a5-mkdite6i",
|
||||||
|
"name": "青蓝虚描",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": true,
|
||||||
|
"backgroundBlurIntensity": 100,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 56.73611111111111,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 35.87962962962963,
|
||||||
|
"y": 71.52777777777777
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": true,
|
||||||
|
"personBorderStyle": "dashed",
|
||||||
|
"personBorderColor": "#149fc2",
|
||||||
|
"personBorderWidth": 14,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 11,
|
||||||
|
"titleText": "托德揭秘",
|
||||||
|
"titleFontFamily": "泼墨体",
|
||||||
|
"titleFontSize": 115,
|
||||||
|
"titleFontWeight": 350,
|
||||||
|
"titleColor": "#149fc2",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 7,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435502861.9124,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 12,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.0488,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 16,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.4207,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 14,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 90.9722222222222,
|
||||||
|
"y": 34.16666666666667
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "vertical",
|
||||||
|
"titleCharSpacing": 50,
|
||||||
|
"titleLineSpacing": 118,
|
||||||
|
"titleMaxCharsPerLine": 10,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "rectangle",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "芒格投资稳的原因",
|
||||||
|
"subtitleFontFamily": "杨任东竹石体",
|
||||||
|
"subtitleFontSize": 87,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#149fc2",
|
||||||
|
"subtitleStrokeColor": "#000000",
|
||||||
|
"subtitleStrokeWidth": 0,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1768540523070.3923,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 2,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1768540523070.0796,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 2,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 76.85185185185186,
|
||||||
|
"y": 58.40277777777778
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "vertical",
|
||||||
|
"subtitleCharSpacing": 12,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 15,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
176
templates/cover-template-黄白实描.json
Normal file
176
templates/cover-template-黄白实描.json
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "封面模板导出配置",
|
||||||
|
"timestamp": "2026-06-02T12:13:46.948Z",
|
||||||
|
"coverTemplate": {
|
||||||
|
"id": "485183be-6eed-4eae-b74a-8591f08b69fa",
|
||||||
|
"name": "黄白实描",
|
||||||
|
"config": {
|
||||||
|
"backgroundEnabled": false,
|
||||||
|
"backgroundImagePath": null,
|
||||||
|
"backgroundBlurEnabled": true,
|
||||||
|
"backgroundBlurIntensity": 30,
|
||||||
|
"backgroundSize": 100,
|
||||||
|
"backgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"personImagePath": null,
|
||||||
|
"personSize": 70.27777777777777,
|
||||||
|
"personPosition": {
|
||||||
|
"x": 35.87962962962963,
|
||||||
|
"y": 64.75694444444444
|
||||||
|
},
|
||||||
|
"personRotation": 0,
|
||||||
|
"personBorderEnabled": true,
|
||||||
|
"personBorderStyle": "solid",
|
||||||
|
"personBorderColor": "#FFFFFF",
|
||||||
|
"personBorderWidth": 32,
|
||||||
|
"personBorderDashLength": null,
|
||||||
|
"personBorderGapLength": null,
|
||||||
|
"autoSplitText": true,
|
||||||
|
"titleMaxLength": 4,
|
||||||
|
"subtitleMaxLength": 6,
|
||||||
|
"titleText": "置顶96",
|
||||||
|
"titleFontFamily": "1.程荣光刻楷",
|
||||||
|
"titleFontSize": 300,
|
||||||
|
"titleFontWeight": 1000,
|
||||||
|
"titleColor": "#ffde05",
|
||||||
|
"titleStrokeColor": "#000000",
|
||||||
|
"titleStrokeWidth": 0,
|
||||||
|
"titleShadowColor": "#000000",
|
||||||
|
"titleShadowOffsetX": 0,
|
||||||
|
"titleShadowOffsetY": 0,
|
||||||
|
"titleShadowBlur": 0,
|
||||||
|
"titleShadowEnabled": false,
|
||||||
|
"titleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435502861.9124,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 12,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.0488,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 16,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435502861.4207,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 14,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 100,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"titlePosition": {
|
||||||
|
"x": 91.89814814814812,
|
||||||
|
"y": 66.45833333333334
|
||||||
|
},
|
||||||
|
"titleRotation": 0,
|
||||||
|
"titleDirection": "vertical",
|
||||||
|
"titleCharSpacing": -20,
|
||||||
|
"titleLineSpacing": 118,
|
||||||
|
"titleMaxCharsPerLine": 10,
|
||||||
|
"titleBackgroundEnabled": false,
|
||||||
|
"titleBackgroundColor": "#000000",
|
||||||
|
"titleBackgroundOpacity": 70,
|
||||||
|
"titleBackgroundShape": "rectangle",
|
||||||
|
"titleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"titleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 30
|
||||||
|
},
|
||||||
|
"titleBackgroundPoints": [],
|
||||||
|
"titleBackgroundRadius": 10,
|
||||||
|
"titleBackgroundRotation": 0,
|
||||||
|
"subtitleText": "4高丹草新品",
|
||||||
|
"subtitleFontFamily": "USMCCyuanjiantecu",
|
||||||
|
"subtitleFontSize": 117,
|
||||||
|
"subtitleFontWeight": 500,
|
||||||
|
"subtitleColor": "#FFFFFF",
|
||||||
|
"subtitleStrokeColor": "#000000",
|
||||||
|
"subtitleStrokeWidth": 0,
|
||||||
|
"subtitleShadowColor": "#000000",
|
||||||
|
"subtitleShadowOffsetX": 0,
|
||||||
|
"subtitleShadowOffsetY": 0,
|
||||||
|
"subtitleShadowBlur": 0,
|
||||||
|
"subtitleShadowEnabled": false,
|
||||||
|
"subtitleShadowLayers": [
|
||||||
|
{
|
||||||
|
"id": 1765435591571.2886,
|
||||||
|
"offsetX": -2,
|
||||||
|
"offsetY": -2,
|
||||||
|
"blur": 2,
|
||||||
|
"color": "#FFFFFF",
|
||||||
|
"opacity": 60,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.856,
|
||||||
|
"offsetX": 2,
|
||||||
|
"offsetY": 2,
|
||||||
|
"blur": 0,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 65,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1765435591571.3809,
|
||||||
|
"offsetX": 0,
|
||||||
|
"offsetY": 0,
|
||||||
|
"blur": 4,
|
||||||
|
"color": "#000000",
|
||||||
|
"opacity": 50,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subtitlePosition": {
|
||||||
|
"x": 47.45370370370371,
|
||||||
|
"y": 12.743055555555557
|
||||||
|
},
|
||||||
|
"subtitleRotation": 0,
|
||||||
|
"subtitleDirection": "horizontal",
|
||||||
|
"subtitleCharSpacing": 12,
|
||||||
|
"subtitleLineSpacing": 72,
|
||||||
|
"subtitleMaxCharsPerLine": 15,
|
||||||
|
"subtitleBackgroundEnabled": false,
|
||||||
|
"subtitleBackgroundColor": "#000000",
|
||||||
|
"subtitleBackgroundOpacity": 70,
|
||||||
|
"subtitleBackgroundShape": "rectangle",
|
||||||
|
"subtitleBackgroundSize": {
|
||||||
|
"width": 30,
|
||||||
|
"height": 10
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 60
|
||||||
|
},
|
||||||
|
"subtitleBackgroundPoints": [],
|
||||||
|
"subtitleBackgroundRadius": 10,
|
||||||
|
"subtitleBackgroundRotation": 0,
|
||||||
|
"maskEnabled": false,
|
||||||
|
"maskImagePath": null,
|
||||||
|
"maskSize": 100,
|
||||||
|
"maskPosition": {
|
||||||
|
"x": 50,
|
||||||
|
"y": 50
|
||||||
|
},
|
||||||
|
"maskColor": null,
|
||||||
|
"maskOpacity": 100,
|
||||||
|
"maskShape": "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user