add text shadow feature

This commit is contained in:
sun-guannan
2025-08-01 08:57:32 +08:00
parent d478fa9591
commit 1947b250cb
6 changed files with 829 additions and 204 deletions

View File

@@ -5,7 +5,7 @@ from pyJianYingDraft import trange, Font_type
from typing import Optional
from pyJianYingDraft import exceptions
from create_draft import get_or_create_draft
from pyJianYingDraft.text_segment import TextBubble, TextEffect
from pyJianYingDraft.text_segment import TextBubble, TextEffect, TextStyleRange
def add_text_impl(
text: str,
@@ -28,6 +28,18 @@ def add_text_impl(
background_color: str = "#000000",
background_style: int = 1,
background_alpha: float = 0.0, # Default no background display
background_round_radius: float = 0.0, # 背景圆角半径范围0.0-1.0
background_height: float = 0.14, # 背景高度范围0.0-1.0
background_width: float = 0.14, # 背景宽度范围0.0-1.0
background_horizontal_offset: float = 0.5, # 背景水平偏移范围0.0-1.0
background_vertical_offset: float = 0.5, # 背景垂直偏移范围0.0-1.0
# 阴影参数
shadow_enabled: bool = False, # 是否启用阴影
shadow_alpha: float = 0.9, # 阴影透明度范围0.0-1.0
shadow_angle: float = -45.0, # 阴影角度,范围-180.0-180.0
shadow_color: str = "#000000", # 阴影颜色
shadow_distance: float = 5.0, # 阴影距离
shadow_smoothing: float = 0.15, # 阴影平滑度范围0.0-1.0
# Bubble effect
bubble_effect_id: Optional[str] = None,
bubble_resource_id: Optional[str] = None,
@@ -41,6 +53,8 @@ def add_text_impl(
height: int = 1920,
fixed_width: float = -1, # Text fixed width ratio, default -1 means not fixed
fixed_height: float = -1, # Text fixed height ratio, default -1 means not fixed
# 多样式文本参数
text_styles: Optional[List[TextStyleRange]] = None, # 文本的不同部分的样式列表
):
"""
Add text subtitle to the specified draft (configurable parameter version)
@@ -62,6 +76,17 @@ def add_text_impl(
:param background_color: Background color (default black)
:param background_style: Background style (default 1)
:param background_alpha: Background transparency (default 0.0, no background display)
:param background_round_radius: 背景圆角半径范围0.0-1.0默认0.0
:param background_height: 背景高度范围0.0-1.0默认0.14
:param background_width: 背景宽度范围0.0-1.0默认0.14
:param background_horizontal_offset: 背景水平偏移范围0.0-1.0默认0.5
:param background_vertical_offset: 背景垂直偏移范围0.0-1.0默认0.5
:param shadow_enabled: 是否启用阴影默认False
:param shadow_alpha: 阴影透明度范围0.0-1.0默认0.9
:param shadow_angle: 阴影角度,范围-180.0-180.0(默认-45.0
:param shadow_color: 阴影颜色(默认黑色)
:param shadow_distance: 阴影距离默认5.0
:param shadow_smoothing: 阴影平滑度范围0.0-1.0默认0.15
:param bubble_effect_id: Bubble effect ID
:param bubble_resource_id: Bubble resource ID
:param effect_effect_id: Text effect ID
@@ -73,6 +98,7 @@ def add_text_impl(
:param height: Video height (pixels)
:param fixed_width: Text fixed width ratio, range 0.0-1.0, default -1 means not fixed
:param fixed_height: Text fixed height ratio, range 0.0-1.0, default -1 means not fixed
:param text_styles: 文本的不同部分的样式列表每个元素是一个TextStyleRange
:return: Updated draft information
"""
# Validate if font is in Font_type
@@ -130,9 +156,26 @@ def add_text_impl(
text_background = draft.Text_background(
color=background_color,
style=background_style,
alpha=background_alpha
alpha=background_alpha,
round_radius=background_round_radius,
height=background_height,
width=background_width,
horizontal_offset=background_horizontal_offset,
vertical_offset=background_vertical_offset
)
# 创建text_shadow (阴影)
text_shadow = None
if shadow_enabled:
text_shadow = draft.Text_shadow(
has_shadow=shadow_enabled,
alpha=shadow_alpha,
angle=shadow_angle,
color=shadow_color,
distance=shadow_distance,
smoothing=shadow_smoothing
)
# Create bubble effect
text_bubble = None
if bubble_effect_id and bubble_resource_id:
@@ -171,10 +214,21 @@ def add_text_impl(
clip_settings=draft.Clip_settings(transform_y=transform_y, transform_x=transform_x),
border=text_border,
background=text_background,
shadow=text_shadow,
fixed_width=pixel_fixed_width,
fixed_height=pixel_fixed_height
)
# 应用多样式文本设置
if text_styles:
for style_range in text_styles:
# 验证范围有效性
if style_range.start < 0 or style_range.end > len(text) or style_range.start >= style_range.end:
raise ValueError(f"无效的文本范围: [{style_range.start}, {style_range.end}), 文本长度: {len(text)}")
# 应用样式到特定文本范围
text_segment.add_text_style(style_range)
if text_bubble:
text_segment.add_bubble(text_bubble.effect_id, text_bubble.resource_id)
if text_effect:

View File

@@ -349,6 +349,19 @@ def add_text():
background_color = data.get('background_color', "#000000")
background_style = data.get('background_style', 0)
background_alpha = data.get('background_alpha', 0.0)
background_round_radius = data.get('background_round_radius', 0.0)
background_height = data.get('background_height', 0.14) # 背景高度范围0.0-1.0
background_width = data.get('background_width', 0.14) # 背景宽度范围0.0-1.0
background_horizontal_offset = data.get('background_horizontal_offset', 0.5) # 背景水平偏移范围0.0-1.0
background_vertical_offset = data.get('background_vertical_offset', 0.5) # 背景垂直偏移范围0.0-1.0
# 阴影参数
shadow_enabled = data.get('shadow_enabled', False) # 是否启用阴影
shadow_alpha = data.get('shadow_alpha', 0.9) # 阴影透明度范围0.0-1.0
shadow_angle = data.get('shadow_angle', -45.0) # 阴影角度,范围-180.0-180.0
shadow_color = data.get('shadow_color', "#000000") # 阴影颜色
shadow_distance = data.get('shadow_distance', 5.0) # 阴影距离
shadow_smoothing = data.get('shadow_smoothing', 0.15) # 阴影平滑度范围0.0-1.0
# Bubble and decorative text effects
bubble_effect_id = data.get('bubble_effect_id')
@@ -363,6 +376,50 @@ def add_text():
outro_animation = data.get('outro_animation')
outro_duration = data.get('outro_duration', 0.5)
# 新增多样式文本参数
text_styles_data = data.get('text_styles', [])
text_styles = None
if text_styles_data:
text_styles = []
for style_data in text_styles_data:
# 获取样式范围
start_pos = style_data.get('start', 0)
end_pos = style_data.get('end', 0)
# 创建文本样式
style = Text_style(
size=style_data.get('style',{}).get('size', font_size),
bold=style_data.get('style',{}).get('bold', False),
italic=style_data.get('style',{}).get('italic', False),
underline=style_data.get('style',{}).get('underline', False),
color=hex_to_rgb(style_data.get('style',{}).get('color', font_color)),
alpha=style_data.get('style',{}).get('alpha', font_alpha),
align=style_data.get('style',{}).get('align', 1),
vertical=style_data.get('style',{}).get('vertical', vertical),
letter_spacing=style_data.get('style',{}).get('letter_spacing', 0),
line_spacing=style_data.get('style',{}).get('line_spacing', 0)
)
# 创建描边(如果有)
border = None
if style_data.get('border',{}).get('width', 0) > 0:
border = Text_border(
alpha=style_data.get('border',{}).get('alpha', border_alpha),
color=hex_to_rgb(style_data.get('border',{}).get('color', border_color)),
width=style_data.get('border',{}).get('width', border_width)
)
# 创建样式范围对象
style_range = TextStyleRange(
start=start_pos,
end=end_pos,
style=style,
border=border,
font_str=style_data.get('font', font)
)
text_styles.append(style_range)
result = {
"success": False,
"output": "",
@@ -397,6 +454,17 @@ def add_text():
background_color=background_color,
background_style=background_style,
background_alpha=background_alpha,
background_round_radius=background_round_radius,
background_height=background_height,
background_width=background_width,
background_horizontal_offset=background_horizontal_offset,
background_vertical_offset=background_vertical_offset,
shadow_enabled=shadow_enabled,
shadow_alpha=shadow_alpha,
shadow_angle=shadow_angle,
shadow_color=shadow_color,
shadow_distance=shadow_distance,
shadow_smoothing=shadow_smoothing,
bubble_effect_id=bubble_effect_id,
bubble_resource_id=bubble_resource_id,
effect_effect_id=effect_effect_id,
@@ -407,7 +475,8 @@ def add_text():
width=width,
height=height,
fixed_width=fixed_width,
fixed_height=fixed_height
fixed_height=fixed_height,
text_styles = text_styles
)
result["success"] = True

View File

@@ -63,6 +63,7 @@ def download_image(image_url, draft_name, material_name):
# Use ffmpeg to download and convert image to PNG format
command = [
'ffmpeg',
'-headers', 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36\r\nReferer: https://www.163.com/\r\n',
'-i', image_url,
'-vf', 'format=rgba', # Convert to RGBA format to support transparency
'-frames:v', '1', # Ensure only one frame is processed
@@ -150,7 +151,15 @@ def download_file(url:str, local_filename, max_retries=3, timeout=180):
os.makedirs(directory, exist_ok=True)
print(f"Created directory: {directory}")
with requests.get(url, stream=True, timeout=timeout) as response:
# Add headers
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
'Referer': 'https://www.163.com/', # 网易的Referer
'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
}
with requests.get(url, stream=True, timeout=timeout, headers=headers) as response:
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))

View File

@@ -2,15 +2,15 @@ import requests
import json
import sys
import time
from settings.local import PORT
from util import timing_decorator
import functools
import threading
from pyJianYingDraft.text_segment import TextStyleRange, Text_style, Text_border
# Base URL of the service, please modify according to actual situation
BASE_URL = "http://localhost:9000"
LICENSE_KEY = "539C3FEB-74AE48D4-A964D52B-C520F801" # Using trial version license key
BASE_URL = f"http://localhost:{PORT}"
def make_request(endpoint, data, method='POST'):
"""Send HTTP request to the server and handle the response"""
url = f"{BASE_URL}/{endpoint}"
@@ -37,7 +37,6 @@ def add_audio_track(audio_url, start, end, target_start, volume=1.0,
speed=1.0, track_name="main_audio", effect_type=None, effect_params=None, draft_id=None):
"""API call to add audio track"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"audio_url": audio_url,
"start": start,
"end": end,
@@ -54,15 +53,23 @@ def add_audio_track(audio_url, start, end, target_start, volume=1.0,
return make_request("add_audio", data)
def add_text_impl(text, start, end, font, font_color, font_size, track_name,draft_folder="123", draft_id=None,
vertical=False, transform_x=0.5, transform_y=0.5, font_alpha=1.0,
def add_text_impl(text, start, end, font, font_color, font_size, track_name, draft_folder="123", draft_id=None,
vertical=False, transform_x=0, transform_y=0, font_alpha=1.0,
border_color=None, border_width=0.0, border_alpha=1.0,
background_color=None, background_alpha=1.0, background_style=None,
background_round_radius=0.0, background_height=0.14, background_width=0.14,
background_horizontal_offset=0.5, background_vertical_offset=0.5,
shadow_enabled=False, shadow_alpha=0.9, shadow_angle=-45.0,
shadow_color="#000000", shadow_distance=5.0, shadow_smoothing=0.15,
bubble_effect_id=None, bubble_resource_id=None,
effect_effect_id=None, outro_animation=None):
"""API call to add text"""
effect_effect_id=None,
intro_animation=None, intro_duration=0.5,
outro_animation=None, outro_duration=0.5,
width=1080, height=1920,
fixed_width=-1, fixed_height=-1,
text_styles=None):
"""add text"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"draft_folder": draft_folder,
"text": text,
"start": start,
@@ -89,6 +96,21 @@ def add_text_impl(text, start, end, font, font_color, font_size, track_name,draf
data["background_alpha"] = background_alpha
if background_style:
data["background_style"] = background_style
data["background_round_radius"] = background_round_radius
data["background_height"] = background_height
data["background_width"] = background_width
data["background_horizontal_offset"] = background_horizontal_offset
data["background_vertical_offset"] = background_vertical_offset
# Add shadow parameters
if shadow_enabled:
data["shadow_enabled"] = shadow_enabled
data["shadow_alpha"] = shadow_alpha
data["shadow_angle"] = shadow_angle
data["shadow_color"] = shadow_color
data["shadow_distance"] = shadow_distance
data["shadow_smoothing"] = shadow_smoothing
# Add bubble effect parameters
if bubble_effect_id:
@@ -100,23 +122,45 @@ def add_text_impl(text, start, end, font, font_color, font_size, track_name,draf
if effect_effect_id:
data["effect_effect_id"] = effect_effect_id
if draft_id:
data["draft_id"] = draft_id
# Add intro animation parameters
if intro_animation:
data["intro_animation"] = intro_animation
data["intro_duration"] = intro_duration
# Add outro animation parameters
if outro_animation:
data["outro_animation"] = outro_animation
data["outro_duration"] = outro_duration
# Add size parameters
data["width"] = width
data["height"] = height
# Add fixed size parameters
if fixed_width > 0:
data["fixed_width"] = fixed_width
if fixed_height > 0:
data["fixed_height"] = fixed_height
if draft_id:
data["draft_id"] = draft_id
# Add text styles parameters
if text_styles:
data["text_styles"] = text_styles
if draft_id:
data["draft_id"] = draft_id
return make_request("add_text", data)
def add_image_impl(image_url, width, height, start, end, track_name, draft_id=None,
transform_x=0, transform_y=0, scale_x=1.0, scale_y=1.0, transition=None, transition_duration=None,
# New mask-related parameters
mask_type=None, mask_center_x=0.0, mask_center_y=0.0, mask_size=0.5,
mask_rotation=0.0, mask_feather=0.0, mask_invert=False,
mask_rect_width=None, mask_round_corner=None):
mask_rect_width=None, mask_round_corner=None, background_blur=None):
"""API call to add image"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"image_url": image_url,
"width": width,
"height": height,
@@ -143,6 +187,8 @@ def add_image_impl(image_url, width, height, start, end, track_name, draft_id=No
if draft_id:
data["draft_id"] = draft_id
if background_blur:
data["background_blur"] = background_blur
return make_request("add_image", data)
@@ -150,7 +196,6 @@ def generate_image_impl(prompt, width, height, start, end, track_name, draft_id=
transform_x=0, transform_y=0, scale_x=1.0, scale_y=1.0, transition=None, transition_duration=None):
"""API call to add image"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"prompt": prompt,
"width": width,
"height": height,
@@ -176,7 +221,6 @@ def add_sticker_impl(resource_id, start, end, draft_id=None, transform_x=0, tran
width=1080, height=1920):
"""API call to add sticker"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"sticker_id": resource_id,
"start": start,
"end": end,
@@ -208,7 +252,6 @@ def add_video_keyframe_impl(draft_id, track_name, property_type=None, time=None,
2. Batch keyframes: using property_types, times, values parameters (in list form)
"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"draft_id": draft_id,
"track_name": track_name
}
@@ -237,10 +280,9 @@ def add_video_impl(video_url, start=None, end=None, width=None, height=None, tra
# Mask-related parameters
mask_type=None, mask_center_x=0.5, mask_center_y=0.5, mask_size=1.0,
mask_rotation=0.0, mask_feather=0.0, mask_invert=False,
mask_rect_width=None, mask_round_corner=None):
mask_rect_width=None, mask_round_corner=None, background_blur=None):
"""API call to add video track"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"video_url": video_url,
"height": height,
"track_name": track_name,
@@ -272,13 +314,14 @@ def add_video_impl(video_url, start=None, end=None, width=None, height=None, tra
data["width"] = width
if height:
data["height"] = height
if background_blur:
data["background_blur"] = background_blur
return make_request("add_video", data)
def add_effect(effect_type, start, end, draft_id=None, track_name="effect_01",
params=None, width=1080, height=1920):
"""API call to add effect"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"effect_type": effect_type,
"start": start,
"end": end,
@@ -381,6 +424,227 @@ def test_text():
return result3
def test_text_02():
"""测试添加文本"""
# draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
draft_folder = "/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft"
# 测试用例1基本文本添加
print("\n测试:添加基本文本")
text_result = add_text_impl(
text="你好,我是剪映助手",
start=0,
end=3,
font="思源中宋",
font_color="#FF0000", # 红色
track_name="main_text",
transform_y=0.8,
transform_x=0.5,
font_size=30.0
)
print("测试用例1基本文本成功:", text_result)
# 测试用例2竖排文本
result2 = add_text_impl(
draft_id=text_result['output']['draft_id'],
text="竖排文本演示",
start=3,
end=6,
font="云书法三行魏碑体",
font_color="#00FF00", # 绿色
font_size=8.0,
track_name="main_text",
vertical=True, # 启用竖排
transform_y=-0.5,
outro_animation='晕开'
)
print("测试用例2竖排文本成功:", result2)
# 测试用例3带描边和背景的文本
result3 = add_text_impl(
draft_id=result2['output']['draft_id'],
text="描边和背景测试",
start=6,
end=9,
font="思源中宋",
font_color="#FFFFFF", # 白色文字
font_size=24.0,
track_name="main_text",
transform_y=0.0,
transform_x=0.5,
border_color="#FF0000", # 红色描边
border_width=20.0,
border_alpha=1.0,
background_color="#0000FF", # 蓝色背景
background_alpha=0.5, # 半透明背景
background_style=0 # 气泡样式背景
)
print("测试用例3描边和背景成功:", result3)
# 测试用例4使用 TextStyleRange 的多样式文本
# 创建不同的文本样式
style1 = {
"start": 0,
"end": 2,
"style": {
"color": "#FF0000", # 红色
"size": 30,
"bold": True
},
"border": {
"color": "#FFFFFF", # 白色描边
"width": 40,
"alpha": 1.0
},
"font": "思源中宋"
}
style2 = {
"start": 2,
"end": 4,
"style": {
"color": "#00FF00", # 绿色
"size": 25,
"italic": True
},
"font": "挥墨体"
}
style3 = {
"start": 4,
"end": 6,
"style": {
"color": "#0000FF", # 蓝色
"size": 20,
"underline": True
},
"font": "金陵体"
}
# 添加多样式文本
result4 = add_text_impl(
draft_id=result3['output']['draft_id'],
text="多样式\n文本测试",
start=9,
end=12,
font="思源粗宋",
track_name="main_text",
transform_y=0.5,
transform_x=0.5,
font_color="#000000", # 默认黑色
font_size=20.0,
# 使用字典列表而不是 TextStyleRange 对象列表
text_styles=[style1, style2, style3]
)
print("测试用例4多样式文本成功:", result4)
# 最后保存并上传草稿
if result4.get('success') and result4.get('output'):
save_result = save_draft_impl(result4['output']['draft_id'],draft_folder)
print(f"草稿保存结果: {save_result}")
# 返回最后一个测试结果用于后续操作(如果有的话)
return result4
def test_text_03():
"""测试添加文本"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
# draft_folder = "/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft"
# 测试用例1基本文本添加
print("\n测试:添加基本文本")
text_result = add_text_impl(
text="现在支持",
start=0,
end=6,
font="挥墨体",
font_color="#FFFFFF", # 红色
track_name="text_01",
transform_y=0.58,
transform_x=0,
font_size=24.0,
intro_animation="弹入",
intro_duration=0.5
)
print("测试用例1基本文本成功:", text_result)
# 测试用例2带背景参数的文本
result2 = add_text_impl(
draft_id=text_result['output']['draft_id'],
text="文字背景",
start=1.5,
end=6,
font="思源中宋",
font_color="#FFFFFF",
font_size=20.0,
track_name="text_2",
transform_y=0.15,
transform_x=0,
background_color="#0000FF", # 蓝色背景
background_alpha=0.7, # 70%透明度
background_style=1,
background_round_radius=0.5, # 圆角半径
background_height=0.2, # 背景高度
background_width=0.8, # 背景宽度
background_horizontal_offset=0.5, # 水平居中
background_vertical_offset=0.5, # 垂直居中
intro_animation="弹入",
intro_duration=0.5
)
print("测试用例2背景参数成功:", result2)
# 测试用例3带阴影参数的文本
result3 = add_text_impl(
draft_id=result2['output']['draft_id'],
text="文字阴影",
start=3,
end=6,
font="金陵体",
font_color="#FFFF00", # 黄色文字
font_size=25.0,
track_name="text3",
transform_y=-0.16,
transform_x=0,
shadow_enabled=True, # 启用阴影
shadow_alpha=0.8, # 阴影透明度
shadow_angle=-45.0, # 阴影角度
shadow_color="#0000FF", # 蓝色阴影
shadow_distance=10.0, # 阴影距离
shadow_smoothing=0.3, # 阴影平滑度
intro_animation="弹入",
intro_duration=0.5
)
print("测试用例3阴影参数成功:", result3)
# 测试用例4带描边和背景的文本
result4 = add_text_impl(
draft_id=result3['output']['draft_id'],
text="文字描边",
start=4.5,
end=6,
font="思源中宋",
font_color="#FFFFFF", # 白色文字
font_size=24.0,
track_name="text_4",
transform_y=-0.58,
border_color="#FF0000", # 红色描边
border_width=20.0,
border_alpha=1.0,
intro_animation="弹入",
intro_duration=0.5
)
print("测试用例4综合参数成功:", result4)
# 最后保存并上传草稿
if text_result.get('success') and text_result.get('output'):
save_result = save_draft_impl(text_result['output']['draft_id'],draft_folder)
print(f"草稿保存结果: {save_result}")
# 返回最后一个测试结果用于后续操作(如果有的话)
return text_result
def test_image01():
"""Test adding image"""
# draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
@@ -513,6 +777,23 @@ def test_image04():
print(f"Image added successfully! {image_result['output']['draft_id']}")
print(save_draft_impl(image_result['output']['draft_id'], draft_folder))
def test_image05():
"""测试添加图片"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
# draft_folder = "/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft"
print("\n测试添加图片1")
image_result = add_image_impl(
image_url="https://cdn.wanx.aliyuncs.com/wanx/1719234057367822001/text_to_image_v2/d6e33c84d7554146a25b1093b012838b_0.png?x-oss-process=image/resize,w_500/watermark,image_aW1nL3dhdGVyMjAyNDExMjkwLnBuZz94LW9zcy1wcm9jZXNzPWltYWdlL3Jlc2l6ZSxtX2ZpeGVkLHdfMTQ1LGhfMjU=,t_80,g_se,x_10,y_10/format,webp",
width=1920,
height=1080,
start=5.0,
end=10.0,
track_name="image_main",
background_blur=3
)
print(f"添加图片成功!{image_result['output']['draft_id']}")
print(save_draft_impl(image_result['output']['draft_id'], draft_folder))
def test_mask_01():
"""Test adding images to different tracks"""
@@ -1369,6 +1650,28 @@ def test_video_track04():
else:
print("Unable to get draft ID, skipping save operation.")
def test_video_track05():
"""测试添加视频轨道"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
# draft_folder = "/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft"
video_url = "https://cdn.wanx.aliyuncs.com/wanx/1719234057367822001/text_to_video/092faf3c94244973ab752ee1280ba76f.mp4?spm=5176.29623064.0.0.41ed26d6cBOhV3&file=092faf3c94244973ab752ee1280ba76f.mp4" # 替换为实际视频URL
print("\n测试:添加视频轨道")
video_result = add_video_impl(
video_url='https://p26-bot-workflow-sign.byteimg.com/tos-cn-i-mdko3gqilj/07bf6797a1834d75beb05c63293af204.mp4~tplv-mdko3gqilj-image.image?rk3s=81d4c505&x-expires=1782141919&x-signature=2ETX83Swh%2FwKzHeWB%2F9oGq9vqt4%3D&x-wf-file_name=output-997160b5.mp4',
background_blur=2,
width=1920,
height=1080
)
print(f"视频轨道添加结果: {video_result}")
if video_result and 'output' in video_result and 'draft_id' in video_result['output']:
draft_id = video_result['output']['draft_id']
print(f"保存草稿: {save_draft_impl(draft_id, draft_folder)}")
else:
print("无法获取草稿ID跳过保存操作。")
def test_keyframe():
"""Test adding keyframes"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
@@ -1837,152 +2140,6 @@ def test_transition_02():
else:
print("Unable to get draft ID, skipping save operation.")
def test_generate_image01():
"""Test adding image"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
print("\nTest: Adding image 1")
image_result = generate_image_impl(
prompt="An Asian style doodle person floating in rough sea waves labeled 'Job Market', throwing paper boats made of resumes that are sinking, with a bank account notification bubble showing low balance. Atmosphere: Lost, anxious, turbulent. Art style: Minimalist line art, black and white cartoon style, bold outlines, extremely thick lines, expressive emotions, simple doodle, monochromatic. Composition: Wide angle showing person in center of chaotic elements. Lighting: Harsh contrast.",
width=1024,
height=1024,
start=0,
end=5.0,
transform_y=0.7,
scale_x=2.0,
scale_y=1.0,
transform_x=0,
track_name="main"
)
print(f"Image generated successfully! {image_result['output']['draft_id']}")
print(save_draft_impl(image_result['output']['draft_id'], draft_folder))
def generate_speech_impl(texts, draft_id=None, audio_track_name=None, language="Chinese",
speaker_id="爽快思思/Skye",azure_speaker_id=None, speed_ratio=1.0, start_offset=0.0,
end_padding=0.0, interval_time=0.5, volume=1.0, width=1080, height=1920,
add_subtitle=True, text_track_name=None, font="文轩体",
font_color="#ffffff", font_size=8.0, transform_y=-0.8, transform_x=0,
vertical=False, font_alpha=1.0, border_alpha=1.0, border_color="#000000",
border_width=0.0, background_color="#000000", background_style=1,
background_alpha=0.0, bubble_effect_id=None, bubble_resource_id=None,
effect_effect_id=None, intro_animation=None, intro_duration=0.5,
outro_animation=None, outro_duration=0.5):
"""Generate TTS speech and add to draft API call"""
data = {
"license_key": LICENSE_KEY, # Using trial version license key
"texts": texts,
"audio_track_name": audio_track_name,
"language": language,
"speaker_id": speaker_id,
"azure_speaker_id": azure_speaker_id,
"speed_ratio": speed_ratio,
"start_offset": start_offset,
"end_padding": end_padding,
"interval_time": interval_time,
"volume": volume,
"width": width,
"height": height,
"add_subtitle": add_subtitle,
"text_track_name": text_track_name,
"font": font,
"font_color": font_color,
"font_size": font_size,
"transform_y": transform_y,
"transform_x": transform_x,
"vertical": vertical,
"font_alpha": font_alpha,
"border_alpha": border_alpha,
"border_color": border_color,
"border_width": border_width,
"background_color": background_color,
"background_style": background_style,
"background_alpha": background_alpha,
"bubble_effect_id": bubble_effect_id,
"bubble_resource_id": bubble_resource_id,
"effect_effect_id": effect_effect_id,
"intro_animation": intro_animation,
"intro_duration": intro_duration,
"outro_animation": outro_animation,
"outro_duration": outro_duration
}
if draft_id:
data["draft_id"] = draft_id
return make_request("generate_speech", data)
def test_generate_image02():
"""Test adding image"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
print("\nTest: Adding image 1")
image_result = generate_image_impl(
prompt="A cat in the garden",
width=1024,
height=1024,
start=0,
end=5.0,
transform_y=0.7,
scale_x=2.0,
scale_y=1.0,
transform_x=0,
track_name="main"
)
print("\nTest: Adding image 2")
image_result = generate_image_impl(
prompt="3 dogs running in the snow",
draft_id=image_result['output']['draft_id'],
width=576,
height=1024,
start=5.0,
end=10.0,
transform_y=-0.7,
scale_x=2.0,
scale_y=2.0,
transform_x=0,
track_name="main"
)
print(f"Image generated successfully! {image_result['output']['draft_id']}")
print(save_draft_impl(image_result['output']['draft_id'], draft_folder))
@timing_decorator('TTS Speech Generation')
def test_speech_01():
"""Test TTS speech generation and subtitle addition"""
draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft"
print("\nTest: Generate TTS speech and add subtitles")
speech_result = generate_speech_impl(
texts=["Hello everyone, welcome to my video", "Today we will discuss an interesting topic","What to do when your child doesn't want to go to school", "Hope you enjoy this content","Hello everyone, welcome to my video", "Today we will discuss an interesting topic","What to do when your child doesn't want to go to school", "Hope you enjoy this content","Hello everyone, welcome to my video", "Today we will discuss an interesting topic","What to do when your child doesn't want to go to school", "Hope you enjoy this content"],
language="Chinese",
draft_id="123",
speaker_id="渊博小叔",
azure_speaker_id="zh-CN-YunjianNeural",
speed_ratio=1.0,
start_offset=1.0,
end_padding=1.0,
interval_time=0.5,
volume=0.8,
width=1080,
height=1920,
add_subtitle=True,
font="文轩体",
font_color="#ffffff",
font_size=8.0,
transform_y=-0.8,
transform_x=0,
border_width=2.0,
border_color="#000000",
border_alpha=0.8
)
print(f"TTS speech generation result: {speech_result}")
# if speech_result.get('success'):
# # Save draft
# save_result = save_draft_impl(speech_result['output']['draft_id'], draft_folder)
# print(f"Draft saving result: {save_result}")
# else:
# print(f"TTS generation failed: {speech_result.get('error')}")
if __name__ == "__main__":
# test01()
# test02()

View File

@@ -5,7 +5,7 @@ from .time_util import Timerange
from .audio_segment import Audio_segment
from .video_segment import Video_segment, Sticker_segment, Clip_settings
from .effect_segment import Effect_segment, Filter_segment
from .text_segment import Text_segment, Text_style, Text_border, Text_background
from .text_segment import Text_segment, Text_style, Text_border, Text_background, Text_shadow
from .metadata import Font_type
from .metadata import Mask_type
@@ -72,6 +72,7 @@ __all__ = [
"Text_style",
"Text_border",
"Text_background",
"Text_shadow",
"Track_type",
"Shrink_mode",
"Extend_mode",

View File

@@ -4,7 +4,7 @@ import json
import uuid
from copy import deepcopy
from typing import Dict, Tuple, Any
from typing import Dict, Tuple, Any, List
from typing import Union, Optional, Literal
from pyJianYingDraft.metadata.capcut_text_animation_meta import CapCut_Text_intro, CapCut_Text_outro, CapCut_Text_loop_anim
@@ -166,6 +166,51 @@ class Text_background:
"background_vertical_offset": self.vertical_offset,
}
class Text_shadow:
"""文本阴影参数"""
has_shadow: bool
"""是否启用阴影"""
alpha: float
"""阴影不透明度"""
angle: float
"""阴影角度"""
color: str
"""阴影颜色,格式为'#RRGGBB'"""
distance: float
"""阴影距离"""
smoothing: float
"""阴影平滑度"""
def __init__(self, *, has_shadow: bool = False, alpha: float = 0.9, angle: float = -45.0,
color: str = "#000000", distance: float = 5.0, smoothing: float = 0.45):
"""
Args:
has_shadow (`bool`, optional): 是否启用阴影默认为False
alpha (`float`, optional): 阴影不透明度,取值范围[0, 1]默认为0.9
angle (`float`, optional): 阴影角度,取值范围[-180, 180], 默认为-45.0
color (`str`, optional): 阴影颜色,格式为'#RRGGBB',默认为黑色
distance (`float`, optional): 阴影距离默认为5.0
smoothing (`float`, optional): 阴影平滑度,取值范围[0, 1], 默认0.15
"""
self.has_shadow = has_shadow
self.alpha = alpha
self.angle = angle
self.color = color
self.distance = distance
self.smoothing = smoothing
def export_json(self) -> Dict[str, Any]:
"""生成子JSON数据在Text_segment导出时合并到其中"""
return {
"has_shadow": self.has_shadow,
"shadow_alpha": self.alpha,
"shadow_angle": self.angle,
"shadow_color": self.color,
"shadow_distance": self.distance,
"shadow_smoothing": self.smoothing * 3
}
class TextBubble:
"""文本气泡素材, 与滤镜素材本质上一致"""
@@ -200,6 +245,50 @@ class TextEffect(TextBubble):
ret["source_platform"] = 1
return ret
class TextStyleRange:
"""文本样式范围类,用于定义文本特定范围的样式"""
start: int
"""起始位置(包含)"""
end: int
"""结束位置(不包含)"""
style: Text_style
"""字体样式"""
border: Optional[Text_border]
"""文本描边参数None表示无描边"""
font: Optional[Effect_meta]
"""字体设置None表示使用全局字体"""
def __init__(self, start: int, end: int, style: Text_style, border: Optional[Text_border] = None, font_str:str = None):
"""创建文本样式范围
Args:
start (`int`): 起始位置(包含)
end (`int`): 结束位置(不包含)
style (`Text_style`): 字体样式
border (`Text_border`, optional): 文本描边参数默认为None无描边
font (optional): 字体设置默认为None使用全局字体
"""
self.start = start
self.end = end
self.style = style
self.border = border
if font_str:
try:
font_type = getattr(Font_type, font_str).value
except:
available_fonts = [attr for attr in dir(Font_type) if not attr.startswith('_')]
raise ValueError(f"不支持的字体:{font_str}请使用Font_type中的字体之一{available_fonts}")
self.font = font_type
def get_range(self) -> List[int]:
"""获取范围列表
Returns:
`List[int]`: [start, end] 形式的范围列表
"""
return [self.start, self.end]
class Text_segment(Visual_segment):
"""文本片段类, 目前仅支持设置基本的字体样式"""
@@ -215,6 +304,9 @@ class Text_segment(Visual_segment):
background: Optional[Text_background]
"""文本背景参数, None表示无背景"""
shadow: Optional[Text_shadow]
"""文本阴影参数, None表示无阴影"""
bubble: Optional[TextBubble]
"""文本气泡效果, 在放入轨道时加入素材列表中"""
effect: Optional[TextEffect]
@@ -224,11 +316,15 @@ class Text_segment(Visual_segment):
"""固定宽度, -1表示不固定"""
fixed_height: float
"""固定高度, -1表示不固定"""
text_styles: List[TextStyleRange]
"""文本的多种样式列表"""
def __init__(self, text: str, timerange: Timerange, *,
font: Optional[Font_type] = None,
style: Optional[Text_style] = None, clip_settings: Optional[Clip_settings] = None,
border: Optional[Text_border] = None, background: Optional[Text_background] = None,
shadow: Optional[Text_shadow] = None,
fixed_width: int = -1, fixed_height: int = -1):
"""创建文本片段, 并指定其时间信息、字体样式及图像调节设置
@@ -252,12 +348,21 @@ class Text_segment(Visual_segment):
self.style = style or Text_style()
self.border = border
self.background = background
self.shadow = shadow
self.bubble = None
self.effect = None
self.fixed_width = fixed_width
self.fixed_height = fixed_height
self.text_styles = []
# 修改设置特定范围的文本样式的方法
def add_text_style(self, textStyleRange: TextStyleRange) -> "Text_segment":
# 添加新的样式范围
self.text_styles.append(textStyleRange)
return self
@classmethod
def create_from_template(cls, text: str, timerange: Timerange, template: "Text_segment") -> "Text_segment":
@@ -341,10 +446,20 @@ class Text_segment(Visual_segment):
check_flag |= 8
if self.background:
check_flag |= 16
content_json = {
"styles": [
{
if self.shadow and self.shadow.has_shadow: # 如果有阴影且启用了阴影
check_flag |= 32 # 添加阴影标志
# 构建styles数组
styles = []
if self.text_styles:
# 创建一个排序后的样式范围列表
sorted_styles = sorted(self.text_styles, key=lambda x: x.start)
# 检查是否需要在开头添加默认样式
if sorted_styles[0].start > 0:
# 添加从0到第一个样式开始的默认样式
default_style = {
"fill": {
"alpha": 1.0,
"content": {
@@ -355,26 +470,226 @@ class Text_segment(Visual_segment):
}
}
},
"range": [0, len(self.text)],
"range": [0, sorted_styles[0].start],
"size": self.style.size,
"bold": self.style.bold,
"italic": self.style.italic,
"underline": self.style.underline,
"strokes": [self.border.export_json()] if self.border else []
}
],
# 如果有阴影设置,添加到样式中
if self.shadow and self.shadow.has_shadow:
style_item["shadows"] = [
{
"diffuse": self.shadow.smoothing / 6, # diffuse = smoothing/6
"angle": self.shadow.angle,
"content": {
"solid": {
"color": [int(self.shadow.color[1:3], 16)/255,
int(self.shadow.color[3:5], 16)/255,
int(self.shadow.color[5:7], 16)/255]
}
},
"distance": self.shadow.distance,
"alpha": self.shadow.alpha
}
]
# 如果有全局字体设置,添加到样式中
if self.font:
default_style["font"] = {
"id": self.font.resource_id,
"path": "C:/%s.ttf" % self.font.name
}
# 如果有特效设置,添加到样式中
if self.effect:
default_style["effectStyle"] = {
"id": self.effect.effect_id,
"path": "C:" # 并不会真正在此处放置素材文件
}
styles.append(default_style)
# 处理每个样式范围
for i, style_range in enumerate(sorted_styles):
# 添加当前样式范围的样式
style_item = {
"fill": {
"alpha": 1.0,
"content": {
"render_type": "solid",
"solid": {
"alpha": style_range.style.alpha,
"color": list(style_range.style.color)
}
}
},
"range": style_range.get_range(),
"size": style_range.style.size,
"bold": style_range.style.bold,
"italic": style_range.style.italic,
"underline": style_range.style.underline,
"strokes": [style_range.border.export_json()] if style_range.border else []
}
# 如果TextStyleRange有字体设置优先使用它
if hasattr(style_range, 'font') and style_range.font:
style_item["font"] = {
"id": style_range.font.resource_id,
"path": "C:/%s.ttf" % style_range.font.name
}
# 否则,如果有全局字体设置,使用全局字体
elif self.font:
style_item["font"] = {
"id": self.font.resource_id,
"path": "C:/%s.ttf" % self.font.name
}
# 如果有特效设置,添加到样式中
if self.effect:
style_item["effectStyle"] = {
"id": self.effect.effect_id,
"path": "C:" # 并不会真正在此处放置素材文件
}
styles.append(style_item)
# 检查是否需要在当前样式和下一个样式之间添加默认样式
if i < len(sorted_styles) - 1 and style_range.end < sorted_styles[i+1].start:
# 添加从当前样式结束到下一个样式开始的默认样式
gap_style = {
"fill": {
"alpha": 1.0,
"content": {
"render_type": "solid",
"solid": {
"alpha": self.style.alpha,
"color": list(self.style.color)
}
}
},
"range": [style_range.end, sorted_styles[i+1].start],
"size": self.style.size,
"bold": self.style.bold,
"italic": self.style.italic,
"underline": self.style.underline,
"strokes": [self.border.export_json()] if self.border else []
}
# 如果有全局字体设置,添加到样式中
if self.font:
gap_style["font"] = {
"id": self.font.resource_id,
"path": "C:/%s.ttf" % self.font.name
}
# 如果有特效设置,添加到样式中
if self.effect:
gap_style["effectStyle"] = {
"id": self.effect.effect_id,
"path": "C:" # 并不会真正在此处放置素材文件
}
styles.append(gap_style)
# 检查是否需要在最后一个样式之后添加默认样式
if sorted_styles[-1].end < len(self.text):
# 添加从最后一个样式结束到文本结尾的默认样式
end_style = {
"fill": {
"alpha": 1.0,
"content": {
"render_type": "solid",
"solid": {
"alpha": self.style.alpha,
"color": list(self.style.color)
}
}
},
"range": [sorted_styles[-1].end, len(self.text)],
"size": self.style.size,
"bold": self.style.bold,
"italic": self.style.italic,
"underline": self.style.underline,
"strokes": [self.border.export_json()] if self.border else []
}
# 如果有全局字体设置,添加到样式中
if self.font:
end_style["font"] = {
"id": self.font.resource_id,
"path": "C:/%s.ttf" % self.font.name
}
# 如果有特效设置,添加到样式中
if self.effect:
end_style["effectStyle"] = {
"id": self.effect.effect_id,
"path": "C:" # 并不会真正在此处放置素材文件
}
styles.append(end_style)
else:
# 如果text_styles为空使用全局样式创建一个默认的style
style_item = {
"fill": {
"alpha": 1.0,
"content": {
"render_type": "solid",
"solid": {
"alpha": self.style.alpha,
"color": list(self.style.color)
}
}
},
"range": [0, len(self.text)],
"size": self.style.size,
"bold": self.style.bold,
"italic": self.style.italic,
"underline": self.style.underline,
"strokes": [self.border.export_json()] if self.border else []
}
# 如果有阴影设置,添加到样式中
if self.shadow and self.shadow.has_shadow:
style_item["shadows"] = [
{
"diffuse": self.shadow.smoothing / 6, # diffuse = smoothing/6
"angle": self.shadow.angle,
"content": {
"solid": {
"color": [int(self.shadow.color[1:3], 16)/255,
int(self.shadow.color[3:5], 16)/255,
int(self.shadow.color[5:7], 16)/255]
}
},
"distance": self.shadow.distance,
"alpha": self.shadow.alpha
}
]
# 如果有全局字体设置,添加到样式中
if self.font:
style_item["font"] = {
"id": self.font.resource_id,
"path": "C:/%s.ttf" % self.font.name # 并不会真正在此处放置字体文件
}
# 如果有特效设置,添加到样式中
if self.effect:
style_item["effectStyle"] = {
"id": self.effect.effect_id,
"path": "C:" # 并不会真正在此处放置素材文件
}
styles.append(style_item)
content_json = {
"styles": styles,
"text": self.text
}
if self.font:
content_json["styles"][0]["font"] = {
"id": self.font.resource_id,
"path": "C:/%s.ttf" % self.font.name # 并不会真正在此处放置字体文件
}
if self.effect:
content_json["styles"][0]["effectStyle"] = {
"id": self.effect.effect_id,
"path": "C:" # 并不会真正在此处放置素材文件
}
ret = {
"id": self.material_id,
@@ -413,19 +728,34 @@ class Text_segment(Visual_segment):
# },
# "shadow_smoothing": 0.45,
# 整体字体设置, 似乎会被content覆盖
# "font_category_id": "",
# "font_category_name": "",
# "font_id": "",
# "font_name": "",
# "font_path": "",
# "font_resource_id": "",
# "font_size": 15.0,
# "font_source_platform": 0,
# "font_team_id": "",
# "font_title": "none",
# "font_url": "",
# "fonts": [],
# 整体字体设置
"font_category_id": "",
"font_category_name": "",
"font_id": "",
"font_name": "",
"font_path": "",
"font_resource_id": "",
"font_size": 15.0,
"font_source_platform": 0,
"font_team_id": "",
"font_title": "none",
"font_url": "",
"fonts": [] if not self.text_styles else [
# 根据text_styles生成fonts数组
*[{
"category_id": "preset",
"category_name": "剪映预设",
"effect_id": style_range.font.resource_id if hasattr(style_range, 'font') and style_range.font else (self.font.resource_id if self.font else ""),
"file_uri": "",
"id": "BFBA9655-1FE5-41A0-A85D-577EFFF17BDD",
"path": "C:/%s.ttf" % (style_range.font.name if hasattr(style_range, 'font') and style_range.font else (self.font.name if self.font else "")),
"request_id": "20250713102314DA3D8F267527925ADC9A",
"resource_id": style_range.font.resource_id if hasattr(style_range, 'font') and style_range.font else (self.font.resource_id if self.font else ""),
"source_platform": 0,
"team_id": "",
"title": style_range.font.name if hasattr(style_range, 'font') and style_range.font else (self.font.name if self.font else "")
} for style_range in self.text_styles if (hasattr(style_range, 'font') and style_range.font) or self.font]
],
# 似乎会被content覆盖
# "text_alpha": 1.0,
@@ -438,5 +768,10 @@ class Text_segment(Visual_segment):
if self.background:
ret.update(self.background.export_json())
# 添加阴影参数
if self.shadow and self.shadow.has_shadow:
shadow_json = self.shadow.export_json()
ret.update(shadow_json) # 将阴影参数合并到返回的字典中
return ret