web.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986
  1. from flask import Flask, render_template, request, jsonify, send_file
  2. import pandas as pd
  3. from openpyxl import load_workbook
  4. import os
  5. import shutil
  6. import requests
  7. import json
  8. import re
  9. import time
  10. from datetime import datetime
  11. from collections import Counter
  12. app = Flask(__name__)
  13. app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
  14. BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  15. UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
  16. TEMPLATES_FOLDER = os.path.join(BASE_DIR, 'templates_excel')
  17. OUTPUT_FOLDER = os.path.join(BASE_DIR, 'output')
  18. for folder in [UPLOAD_FOLDER, TEMPLATES_FOLDER, OUTPUT_FOLDER]:
  19. if not os.path.exists(folder):
  20. os.makedirs(folder)
  21. # ========== AI 配置 ==========
  22. AI_CONFIG = {
  23. 'api_url': 'https://ark.cn-beijing.volces.com/api/v3/responses',
  24. 'authorization': 'Bearer ark-f7930e50-4837-4f8f-96b4-bd2f56313300-b9dd7',
  25. 'vision_model': 'doubao-seed-2-1-pro-260628',
  26. 'extract_model': 'deepseek-v4-pro-260425',
  27. }
  28. # ========== AI 属性映射 ==========
  29. AI_FIELD_MAPPING = {
  30. 'Table Top Shape': 'Table Top Shape',
  31. 'Coffee Table Lift Top': 'Coffee Table Lift Top',
  32. 'Upholstered': 'Upholstered',
  33. 'Plug-In': 'Plug-In',
  34. 'Storage Included': 'Storage Included',
  35. 'Shelves Included': 'Shelves Included',
  36. 'Drawers Included': 'Drawers Included',
  37. 'Cabinets Included': 'Cabinets Included',
  38. 'Decal/Laminate Design': 'Decal/Laminate Design',
  39. 'Set Type': 'Set Type',
  40. 'Durability': 'Durability',
  41. 'Number of Tables Included': 'Number of Tables Included',
  42. 'CAL TB 117-2013 Compliant': 'CAL TB 117-2013 Compliant',
  43. 'SOFFA Compliant': 'SOFFA Compliant',
  44. }
  45. # ========== 全局日志存储 ==========
  46. ai_logs = []
  47. def add_ai_log(message, level='info'):
  48. """添加AI日志"""
  49. timestamp = datetime.now().strftime('%H:%M:%S')
  50. log_entry = {
  51. 'time': timestamp,
  52. 'message': message,
  53. 'level': level
  54. }
  55. ai_logs.append(log_entry)
  56. print(f"[AI] {timestamp} - {message}")
  57. # 只保留最近200条
  58. if len(ai_logs) > 200:
  59. ai_logs.pop(0)
  60. def get_ai_logs():
  61. """获取AI日志"""
  62. return ai_logs
  63. def clear_ai_logs():
  64. """清空AI日志"""
  65. global ai_logs
  66. ai_logs = []
  67. # ========== 模板配置 ==========
  68. TEMPLATE_CONFIG = {
  69. 'wayfair': {
  70. 'file': 'wayfair模板.xlsx',
  71. 'source_header': 2,
  72. 'template_header': 4,
  73. 'name': 'Wayfair'
  74. },
  75. 'amazon': {
  76. 'file': '亚马逊模板.xlsx',
  77. 'source_header': 1,
  78. 'template_header': 1,
  79. 'name': '亚马逊'
  80. }
  81. }
  82. def load_valid_values(wb):
  83. valid_data = {}
  84. if "Valid Values" not in wb.sheetnames:
  85. return valid_data
  86. ws_valid = wb["Valid Values"]
  87. headers = []
  88. for col in range(1, ws_valid.max_column + 1):
  89. val = ws_valid.cell(row=1, column=col).value
  90. if val:
  91. headers.append(str(val).strip())
  92. else:
  93. headers.append(f"Column_{col}")
  94. for row in range(2, ws_valid.max_row + 1):
  95. for col in range(1, len(headers) + 1):
  96. val = ws_valid.cell(row=row, column=col).value
  97. if val:
  98. header = headers[col - 1]
  99. if header not in valid_data:
  100. valid_data[header] = []
  101. if str(val).strip() not in valid_data[header]:
  102. valid_data[header].append(str(val).strip())
  103. return valid_data
  104. def get_valid_value(valid_values, field_name, default=None):
  105. if field_name in valid_values and valid_values[field_name]:
  106. return valid_values[field_name][0]
  107. return default
  108. def call_vision_api(image_url, row_index):
  109. """调用豆包视觉模型识别图片(带详细日志)"""
  110. add_ai_log(f"🖼️ [第{row_index}行] 开始识别图片: {image_url[:60]}...", 'info')
  111. headers = {
  112. 'Accept': '*/*',
  113. 'Accept-Encoding': 'gzip, deflate, br',
  114. 'Authorization': AI_CONFIG['authorization'],
  115. 'Connection': 'keep-alive',
  116. 'Content-Type': 'application/json',
  117. 'User-Agent': 'PostmanRuntime-ApipostRuntime/1.1.0'
  118. }
  119. data = {
  120. "model": AI_CONFIG['vision_model'],
  121. "input": [
  122. {
  123. "role": "user",
  124. "content": [
  125. {
  126. "type": "input_image",
  127. "image_url": image_url
  128. },
  129. {
  130. "type": "input_text",
  131. "text": "请详细描述这张图片中的家具,包括:形状、颜色、材质、结构、功能特点、适用场景。描述要详细具体。"
  132. }
  133. ]
  134. }
  135. ]
  136. }
  137. add_ai_log(f"📤 [第{row_index}行] 调用视觉模型 ({AI_CONFIG['vision_model']})...", 'info')
  138. start_time = time.time()
  139. try:
  140. response = requests.post(
  141. AI_CONFIG['api_url'],
  142. headers=headers,
  143. json=data,
  144. timeout=120
  145. )
  146. elapsed = time.time() - start_time
  147. add_ai_log(f"⏱️ [第{row_index}行] 视觉识别耗时: {elapsed:.1f}秒", 'info')
  148. result = response.json()
  149. if 'output' in result:
  150. for item in result['output']:
  151. if item.get('type') == 'message' and 'content' in item:
  152. for content in item['content']:
  153. if content.get('type') == 'output_text':
  154. text = content.get('text', '')
  155. add_ai_log(f"✅ [第{row_index}行] 视觉识别成功,描述长度: {len(text)} 字符", 'success')
  156. return text
  157. add_ai_log(f"⚠️ [第{row_index}行] 视觉识别返回格式异常", 'warn')
  158. return None
  159. except requests.exceptions.Timeout:
  160. add_ai_log(f"❌ [第{row_index}行] 视觉识别超时 (120秒)", 'error')
  161. return None
  162. except Exception as e:
  163. add_ai_log(f"❌ [第{row_index}行] 视觉识别失败: {str(e)}", 'error')
  164. return None
  165. def call_extract_api(description, row_index):
  166. """调用DeepSeek提取属性(带详细日志)"""
  167. add_ai_log(f"🧠 [第{row_index}行] 开始属性提取...", 'info')
  168. headers = {
  169. 'Accept': '*/*',
  170. 'Accept-Encoding': 'gzip, deflate, br',
  171. 'Authorization': AI_CONFIG['authorization'],
  172. 'Connection': 'keep-alive',
  173. 'Content-Type': 'application/json',
  174. 'User-Agent': 'PostmanRuntime-ApipostRuntime/1.1.0'
  175. }
  176. fields = [
  177. 'Table Top Shape',
  178. 'Coffee Table Lift Top',
  179. 'Upholstered',
  180. 'Plug-In',
  181. 'Storage Included',
  182. 'Shelves Included',
  183. 'Drawers Included',
  184. 'Cabinets Included',
  185. 'Decal/Laminate Design',
  186. 'Set Type',
  187. 'Durability',
  188. 'Number of Tables Included',
  189. 'CAL TB 117-2013 Compliant',
  190. 'SOFFA Compliant'
  191. ]
  192. fields_str = ', '.join(fields)
  193. data = {
  194. "model": AI_CONFIG['extract_model'],
  195. "stream": False,
  196. "tools": [
  197. {
  198. "type": "web_search",
  199. "max_keyword": 3
  200. }
  201. ],
  202. "input": [
  203. {
  204. "role": "user",
  205. "content": [
  206. {
  207. "type": "input_text",
  208. "text": f"""请根据下面的家具描述,填写以下信息:{fields_str}。
  209. 返回格式要求:每个属性用方括号括起来,格式为 [属性名:值]
  210. 例如:[Table Top Shape:Rectangle with rounded corners] [Coffee Table Lift Top:No]
  211. 如果描述中没有相关信息,请填写 "Unknown"。
  212. 家具描述:
  213. {description}"""
  214. }
  215. ]
  216. }
  217. ]
  218. }
  219. add_ai_log(f"📤 [第{row_index}行] 调用提取模型 ({AI_CONFIG['extract_model']})...", 'info')
  220. start_time = time.time()
  221. try:
  222. response = requests.post(
  223. AI_CONFIG['api_url'],
  224. headers=headers,
  225. json=data,
  226. timeout=120
  227. )
  228. elapsed = time.time() - start_time
  229. add_ai_log(f"⏱️ [第{row_index}行] 属性提取耗时: {elapsed:.1f}秒", 'info')
  230. result = response.json()
  231. if 'output' in result:
  232. for item in result['output']:
  233. if item.get('type') == 'message' and 'content' in item:
  234. for content in item['content']:
  235. if content.get('type') == 'output_text':
  236. text = content.get('text', '')
  237. add_ai_log(f"✅ [第{row_index}行] 属性提取成功,结果长度: {len(text)} 字符", 'success')
  238. return text
  239. add_ai_log(f"⚠️ [第{row_index}行] 属性提取返回格式异常", 'warn')
  240. return None
  241. except requests.exceptions.Timeout:
  242. add_ai_log(f"❌ [第{row_index}行] 属性提取超时 (120秒)", 'error')
  243. return None
  244. except Exception as e:
  245. add_ai_log(f"❌ [第{row_index}行] 属性提取失败: {str(e)}", 'error')
  246. return None
  247. def parse_ai_result(text):
  248. """解析AI返回的 [属性:值] 格式"""
  249. result = {}
  250. if not text:
  251. return result
  252. pattern = r'\[([^:\]]+):([^\]]+)\]'
  253. matches = re.findall(pattern, text)
  254. for field, value in matches:
  255. field = field.strip()
  256. value = value.strip()
  257. if value and value not in ['Unknown', 'Not Applicable', 'N/A', '']:
  258. result[field] = value
  259. return result
  260. def get_ai_filled_data(image_url, row_index):
  261. """完整的AI填写流程(带详细日志)"""
  262. add_ai_log(f"🚀 [第{row_index}行] 开始AI处理", 'info')
  263. add_ai_log(f"📷 [第{row_index}行] 图片URL: {image_url[:80]}...", 'info')
  264. # 第一步:识别图片
  265. description = call_vision_api(image_url, row_index)
  266. if not description:
  267. add_ai_log(f"⚠️ [第{row_index}行] 图片识别失败,跳过该行", 'warn')
  268. return {}
  269. # 显示描述前100字符
  270. desc_preview = description[:100] + "..." if len(description) > 100 else description
  271. add_ai_log(f"📝 [第{row_index}行] 图片描述预览: {desc_preview}", 'info')
  272. # 第二步:提取属性
  273. extract_result = call_extract_api(description, row_index)
  274. if not extract_result:
  275. add_ai_log(f"⚠️ [第{row_index}行] 属性提取失败,跳过该行", 'warn')
  276. return {}
  277. # 第三步:解析结果
  278. parsed = parse_ai_result(extract_result)
  279. if parsed:
  280. add_ai_log(f"📋 [第{row_index}行] 解析成功,提取到 {len(parsed)} 个字段:", 'success')
  281. for field, value in parsed.items():
  282. add_ai_log(f" └─ {field}: {value}", 'info')
  283. else:
  284. add_ai_log(f"⚠️ [第{row_index}行] 解析结果为空", 'warn')
  285. return parsed
  286. def fill_amazon_row(src_row, source_map, col_indices, valid_values, row_num, ws, suffix, mfr_suffix, ai_data=None):
  287. fill_count = 0
  288. item_code = ""
  289. product_name = ""
  290. product_name_en = ""
  291. try:
  292. item_code = src_row.get("Item Code", "")
  293. product_name = src_row.get("产品名称", "")
  294. product_name_en = src_row.get("产品英文名称", "")
  295. if pd.isna(item_code):
  296. item_code = ""
  297. if pd.isna(product_name):
  298. product_name = ""
  299. if pd.isna(product_name_en):
  300. product_name_en = ""
  301. # SKU 相关
  302. sku_value = f"{item_code}{product_name_en}{suffix}"
  303. if "SKU" in col_indices:
  304. ws.cell(row=row_num, column=col_indices["SKU"], value=sku_value)
  305. fill_count += 1
  306. if "Model Number" in col_indices:
  307. ws.cell(row=row_num, column=col_indices["Model Number"], value=sku_value)
  308. fill_count += 1
  309. if "Model Name" in col_indices:
  310. ws.cell(row=row_num, column=col_indices["Model Name"], value=sku_value)
  311. fill_count += 1
  312. if "Part Number" in col_indices:
  313. ws.cell(row=row_num, column=col_indices["Part Number"], value=sku_value)
  314. fill_count += 1
  315. if "Set Name" in col_indices:
  316. ws.cell(row=row_num, column=col_indices["Set Name"], value=product_name_en if product_name_en else product_name)
  317. fill_count += 1
  318. # 从来源映射
  319. field_map = {
  320. "Color": "颜色",
  321. "Base Color": "颜色",
  322. "Top Color": "颜色",
  323. "Frame Material": "材质",
  324. "Base Material": "材质",
  325. "Top Material": "材质",
  326. "Furniture Leg Material": "材质",
  327. "Upholstery Fabric Type": "材质",
  328. "Item Length": "组装长度",
  329. "Item Width": "组装宽度",
  330. "Item Height": "组装高度",
  331. "Item Weight": "产品重量",
  332. "Item Package Length": "包装尺寸-长度",
  333. "Item Package Width": "包装尺寸-宽度",
  334. "Item Package Height": "包装尺寸-高度",
  335. "Package Weight": "包装尺寸-重量",
  336. "List Price": "优惠单价",
  337. "Your Price USD": "优惠单价",
  338. "Unit Count": "包装尺寸-重量",
  339. "Item Length Longer Edge": "组装长度",
  340. "Item Width Shorter Edge": "组装宽度",
  341. "Length longer edge when assembled": "组装长度",
  342. "Width shorter edge when assembled": "组装宽度",
  343. "Height base to top when assembled": "组装高度",
  344. "Item Depth Front To Back": "组装宽度",
  345. "Item Height Floor To Top": "组装高度",
  346. "Item Width Side To Side": "组装长度",
  347. "Working Surface Length": "组装长度",
  348. "Working Surface Width": "组装宽度",
  349. "Country of Origin": "原产地",
  350. }
  351. source_col_map = {
  352. "颜色": "颜色",
  353. "材质": "材质",
  354. "组装长度": "组装长度(英寸)",
  355. "组装宽度": "组装宽度(英寸)",
  356. "组装高度": "组装高度(英寸)",
  357. "产品重量": "产品重量(磅)",
  358. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  359. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  360. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  361. "包装尺寸-重量": "包装尺寸-重量(磅)",
  362. "优惠单价": "优惠单价",
  363. "产品名称": "产品名称",
  364. "产品英文名称": "产品英文名称",
  365. "Item Code": "Item Code",
  366. "店铺名称": "店铺名称",
  367. "原产地": "原产地",
  368. "产品主图": "产品主图",
  369. "上架图片1": "上架图片1",
  370. "上架图片2": "上架图片2",
  371. "上架图片3": "上架图片3",
  372. "上架图片4": "上架图片4",
  373. "上架图片5": "上架图片5",
  374. "上架图片6": "上架图片6",
  375. "上架图片7": "上架图片7",
  376. "上架图片8": "上架图片8",
  377. "产品描述": "产品描述",
  378. "产品特点1": "产品特点1",
  379. "产品特点2": "产品特点2",
  380. "产品特点3": "产品特点3",
  381. "产品特点4": "产品特点4",
  382. "产品特点5": "产品特点5",
  383. }
  384. for amazon_field, source_key in field_map.items():
  385. if amazon_field not in col_indices:
  386. continue
  387. col_idx = col_indices[amazon_field]
  388. source_col_name = source_col_map.get(source_key, source_key)
  389. source_col = source_map.get(source_key, source_col_name)
  390. if source_col and source_col in src_row:
  391. val = src_row[source_col]
  392. if pd.notna(val):
  393. if amazon_field == "Country of Origin":
  394. val = str(val).strip()
  395. if val in ["CHN", "CN"]:
  396. val = "China"
  397. ws.cell(row=row_num, column=col_idx, value=val)
  398. fill_count += 1
  399. # AI 填充
  400. if ai_data:
  401. for ai_field, ai_value in ai_data.items():
  402. target_field = AI_FIELD_MAPPING.get(ai_field)
  403. if target_field and target_field in col_indices:
  404. ws.cell(row=row_num, column=col_indices[target_field], value=ai_value)
  405. fill_count += 1
  406. # 图片
  407. image_fields = [
  408. ("Main Image URL", "产品主图"),
  409. ("Other Image URL", "上架图片1"),
  410. ("Other Image URL", "上架图片2"),
  411. ("Other Image URL", "上架图片3"),
  412. ("Other Image URL", "上架图片4"),
  413. ("Other Image URL", "上架图片5"),
  414. ("Other Image URL", "上架图片6"),
  415. ("Other Image URL", "上架图片7"),
  416. ("Other Image URL", "上架图片8"),
  417. ]
  418. for field_name, source_key in image_fields:
  419. if field_name not in col_indices:
  420. continue
  421. col_idx = col_indices[field_name]
  422. source_col = source_col_map.get(source_key, source_key)
  423. if source_col in src_row:
  424. val = src_row[source_col]
  425. if pd.notna(val) and str(val).strip():
  426. ws.cell(row=row_num, column=col_idx, value=val)
  427. fill_count += 1
  428. # 固定值
  429. fixed_values = {
  430. "Product Type": "TABLE",
  431. "Listing Action": "Create or Replace (Full Update)",
  432. "Package Level": "Unit",
  433. "Package Contains SKU Quantity": 1,
  434. "Number of Items": 1,
  435. "Item Package Quantity": 1,
  436. "Unit Count Type": "pound",
  437. "Item Thickness Decimal Value": 0.6,
  438. "Item Length Unit": "Inches",
  439. "Item Width Unit": "Inches",
  440. "Item Height Unit": "Inches",
  441. "Item Package Length Unit": "Inches",
  442. "Item Package Width Unit": "Inches",
  443. "Item Package Height Unit": "Inches",
  444. "Package Weight Unit": "Pounds",
  445. "Item Weight Unit": "Pounds",
  446. "Recommended Number of People for Assembly": 2,
  447. "Has Finished Back": "TRUE",
  448. "Includes All Assembly Tools": "Yes",
  449. "Wood Type": "Character",
  450. "Number of Players": 2,
  451. "Is Fragile?": "No",
  452. "Load Capacity Unit": "Pound",
  453. "Surface Texture": "wood grain",
  454. "Maximum Order Quantity": 1,
  455. "Handling Time (US)": 3,
  456. "Number of Boxes": 1,
  457. "Is This Product Subject To Buyer Age Restrictions": "No",
  458. "Safety Attestation": "Yes",
  459. "Ships Globally": "Yes",
  460. "Are batteries required?": "No",
  461. "Are batteries included?": "No",
  462. "Is OEM Sourced Product": "Yes",
  463. "Is Customizable?": "No",
  464. "Is Foldable": "No",
  465. "Is Stain Resistant": "Yes",
  466. "Tilting": "No",
  467. "Offering Can Be Gift Messaged": "Yes",
  468. "Is Gift Wrap Available": "Yes",
  469. "Accessories": "Generic",
  470. "Packaging": "OEM Original",
  471. "Fulfillment Channel Code (US)": "DEFAULT",
  472. "Inventory Always Available (US)": "Disabled",
  473. }
  474. for field_name, fixed_val in fixed_values.items():
  475. if field_name in col_indices:
  476. ws.cell(row=row_num, column=col_indices[field_name], value=fixed_val)
  477. fill_count += 1
  478. # Valid Values
  479. valid_fields = {
  480. "Product Type": "Product Type",
  481. "Listing Action": "Listing Action",
  482. "Brand Name": "Brand Name",
  483. "Product Id Type": "Product Id Type",
  484. "Item Type Keyword": "Item Type Keyword",
  485. "Package Level": "Package Level",
  486. "Warranty Type": "Warranty Type",
  487. "Theme": "Theme",
  488. "Frame Joint Type": "Frame Joint Type",
  489. "Assembly Instructions": "Assembly Instructions",
  490. "Specific Uses for Product": "Specific Uses for Product",
  491. "Room Type": "Room Type",
  492. "Maximum Weight Recommendation Unit": "Maximum Weight Recommendation Unit",
  493. "Leg Style": "Leg Style",
  494. "Indoor Outdoor Usage": "Indoor Outdoor Usage",
  495. "Table Design": "Table Design",
  496. "Furniture Base Movement": "Furniture Base Movement",
  497. "Item Condition": "Item Condition",
  498. "Natural Variation Type": "Natural Variation Type",
  499. "Includes All Assembly Tools": "Includes All Assembly Tools",
  500. "Dangerous Goods Regulations": "Dangerous Goods Regulations",
  501. "Warranty Description": "Warranty Description",
  502. }
  503. for field_name, valid_key in valid_fields.items():
  504. if field_name in col_indices:
  505. val = get_valid_value(valid_values, valid_key)
  506. if val:
  507. ws.cell(row=row_num, column=col_indices[field_name], value=val)
  508. fill_count += 1
  509. # Care Instructions
  510. care_vals = valid_values.get("Care Instructions", ["Wipe with Dry Cloth", "Wipe with Damp Cloth"])
  511. for i in range(5):
  512. field = f"Care Instructions {i+1}" if i > 0 else "Care Instructions"
  513. if field in col_indices:
  514. if i % 2 == 0:
  515. val = care_vals[0] if care_vals else "Wipe with Dry Cloth"
  516. else:
  517. val = care_vals[1] if len(care_vals) > 1 else "Wipe with Damp Cloth"
  518. ws.cell(row=row_num, column=col_indices[field], value=val)
  519. fill_count += 1
  520. # Theme
  521. theme_val = get_valid_value(valid_values, "Theme", "Space")
  522. for i in range(5):
  523. field = f"Theme {i+1}" if i > 0 else "Theme"
  524. if field in col_indices:
  525. ws.cell(row=row_num, column=col_indices[field], value=theme_val)
  526. fill_count += 1
  527. # Warranty Description
  528. warranty_val = get_valid_value(valid_values, "Warranty Description", "1 Year Manufacturer")
  529. for i in range(5):
  530. field = f"Warranty Description {i+1}" if i > 0 else "Warranty Description"
  531. if field in col_indices:
  532. ws.cell(row=row_num, column=col_indices[field], value=warranty_val)
  533. fill_count += 1
  534. # Dangerous Goods Regulations
  535. dgr_val = get_valid_value(valid_values, "Dangerous Goods Regulations", "Not Applicable")
  536. for i in range(5):
  537. field = f"Dangerous Goods Regulations {i+1}" if i > 0 else "Dangerous Goods Regulations"
  538. if field in col_indices:
  539. ws.cell(row=row_num, column=col_indices[field], value=dgr_val)
  540. fill_count += 1
  541. # Tools
  542. tools = ["Hammer", "Screw Driver"]
  543. if "Tools Recommended For Assembly" in col_indices:
  544. for i, tool in enumerate(tools):
  545. if i == 0:
  546. field = "Tools Recommended For Assembly"
  547. else:
  548. field = f"Tools Recommended For Assembly {i+1}"
  549. if field in col_indices:
  550. ws.cell(row=row_num, column=col_indices[field], value=tool)
  551. fill_count += 1
  552. # Components
  553. components = ["Installation Tool", "Assembly Guide"]
  554. if "Included Components" in col_indices:
  555. for i, comp in enumerate(components):
  556. if i == 0:
  557. field = "Included Components"
  558. else:
  559. field = f"Included Components {i+1}"
  560. if field in col_indices:
  561. ws.cell(row=row_num, column=col_indices[field], value=comp)
  562. fill_count += 1
  563. except Exception as e:
  564. print(f"fill_amazon_row error: {e}")
  565. return fill_count
  566. @app.route('/')
  567. def index():
  568. return render_template('index.html')
  569. @app.route('/get_config')
  570. def get_config():
  571. return jsonify({
  572. 'templates': [
  573. {'key': 'wayfair', 'name': 'Wayfair'},
  574. {'key': 'amazon', 'name': '亚马逊'}
  575. ],
  576. 'config': TEMPLATE_CONFIG
  577. })
  578. @app.route('/get_ai_logs')
  579. def get_ai_logs_api():
  580. """获取AI日志API"""
  581. return jsonify({
  582. 'logs': ai_logs[-50:] # 返回最近50条
  583. })
  584. @app.route('/clear_ai_logs')
  585. def clear_ai_logs_api():
  586. """清空AI日志"""
  587. clear_ai_logs()
  588. return jsonify({'success': True})
  589. @app.route('/preview', methods=['POST'])
  590. def preview():
  591. try:
  592. source_file = request.files.get('source_file')
  593. template_key = request.form.get('template_key')
  594. if not source_file:
  595. return jsonify({'error': '请选择来源文件'}), 400
  596. if not template_key or template_key not in TEMPLATE_CONFIG:
  597. return jsonify({'error': '请选择有效的模板'}), 400
  598. config = TEMPLATE_CONFIG[template_key]
  599. source_header = config['source_header']
  600. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  601. source_path = os.path.join(UPLOAD_FOLDER, f'source_{timestamp}_{source_file.filename}')
  602. source_file.save(source_path)
  603. df = pd.read_excel(source_path, header=source_header - 1)
  604. preview_data = df.head(10).fillna('').to_dict('records')
  605. columns = df.columns.tolist()
  606. return jsonify({
  607. 'success': True,
  608. 'columns': columns,
  609. 'data': preview_data,
  610. 'total_rows': len(df),
  611. 'source_path': source_path,
  612. 'config': config
  613. })
  614. except Exception as e:
  615. return jsonify({'error': str(e)}), 500
  616. @app.route('/generate', methods=['POST'])
  617. def generate():
  618. try:
  619. source_path = request.form.get('source_path')
  620. template_key = request.form.get('template_key')
  621. use_ai = request.form.get('use_ai', 'false').lower() == 'true'
  622. if not source_path or not os.path.exists(source_path):
  623. return jsonify({'error': '来源文件不存在'}), 400
  624. if not template_key or template_key not in TEMPLATE_CONFIG:
  625. return jsonify({'error': '请选择有效的模板'}), 400
  626. # 清空之前的AI日志
  627. clear_ai_logs()
  628. add_ai_log(f"🚀 开始生成任务,模板: {template_key},AI: {'启用' if use_ai else '关闭'}", 'info')
  629. config = TEMPLATE_CONFIG[template_key]
  630. template_name = config['file']
  631. source_header = config['source_header']
  632. template_header_row = config['template_header']
  633. template_path = os.path.join(TEMPLATES_FOLDER, template_name)
  634. if not os.path.exists(template_path):
  635. return jsonify({'error': f'模板文件不存在: {template_name}'}), 400
  636. # 读取来源数据
  637. add_ai_log(f"📖 读取来源数据: {os.path.basename(source_path)}", 'info')
  638. df = pd.read_excel(source_path, header=source_header - 1)
  639. total_rows = len(df)
  640. add_ai_log(f"📊 共 {total_rows} 行数据", 'info')
  641. # AI处理
  642. ai_results = {}
  643. if use_ai and '产品主图' in df.columns:
  644. add_ai_log(f"🤖 开始AI识别,共 {total_rows} 行需要处理", 'info')
  645. # 统计有图片的行
  646. has_image_count = 0
  647. for idx, row in df.iterrows():
  648. img_url = row.get('产品主图', '')
  649. if pd.notna(img_url) and str(img_url).strip():
  650. has_image_count += 1
  651. add_ai_log(f"📷 其中 {has_image_count} 行包含产品主图", 'info')
  652. processed = 0
  653. success_count = 0
  654. for idx, row in df.iterrows():
  655. img_url = row.get('产品主图', '')
  656. if pd.notna(img_url) and str(img_url).strip():
  657. processed += 1
  658. add_ai_log(f"🔄 [第{idx+1}/{total_rows}行] 处理中 ({processed}/{has_image_count})...", 'info')
  659. ai_data = get_ai_filled_data(str(img_url).strip(), idx + 1)
  660. if ai_data:
  661. ai_results[idx] = ai_data
  662. success_count += 1
  663. add_ai_log(f"✅ [第{idx+1}行] AI识别完成,提取 {len(ai_data)} 个字段", 'success')
  664. # 每处理5行输出一次汇总
  665. if processed % 5 == 0:
  666. add_ai_log(f"📊 进度: {processed}/{has_image_count} 行已处理,成功 {success_count} 行", 'info')
  667. add_ai_log(f"🎉 AI处理完成!共处理 {processed} 行,成功 {success_count} 行", 'success')
  668. elif use_ai and '产品主图' not in df.columns:
  669. add_ai_log(f"⚠️ 来源数据中没有 '产品主图' 列,AI功能将跳过", 'warn')
  670. else:
  671. add_ai_log(f"⏭️ AI功能未启用", 'info')
  672. # 加载模板
  673. add_ai_log(f"📖 加载模板: {template_name}", 'info')
  674. wb = load_workbook(template_path)
  675. ws = wb.active
  676. valid_values = load_valid_values(wb)
  677. add_ai_log(f"📋 Valid Values 加载完成,共 {len(valid_values)} 个字段", 'info')
  678. # 读取模板表头
  679. col_indices = {}
  680. for col in range(1, ws.max_column + 1):
  681. cell_value = ws.cell(row=template_header_row, column=col).value
  682. if cell_value:
  683. col_name = str(cell_value).strip()
  684. if col_name and not col_name.startswith("Unnamed"):
  685. col_indices[col_name] = col
  686. add_ai_log(f"📋 模板共 {len(col_indices)} 个列", 'info')
  687. # 找到已有数据的最后一行
  688. start_row = 2
  689. last_row = start_row
  690. for row in range(start_row, ws.max_row + 2):
  691. is_empty = True
  692. for col in range(1, min(10, ws.max_column + 1)):
  693. if ws.cell(row=row, column=col).value is not None:
  694. is_empty = False
  695. break
  696. if is_empty:
  697. last_row = row
  698. break
  699. else:
  700. last_row = ws.max_row + 1
  701. add_ai_log(f"📌 已有数据到第 {last_row - 1} 行,从第 {last_row} 行开始追加", 'info')
  702. # 匹配来源列
  703. source_col_map = {
  704. "颜色": "颜色",
  705. "材质": "材质",
  706. "组装长度": "组装长度(英寸)",
  707. "组装宽度": "组装宽度(英寸)",
  708. "组装高度": "组装高度(英寸)",
  709. "产品重量": "产品重量(磅)",
  710. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  711. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  712. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  713. "包装尺寸-重量": "包装尺寸-重量(磅)",
  714. "优惠单价": "优惠单价",
  715. "产品名称": "产品名称",
  716. "产品英文名称": "产品英文名称",
  717. "Item Code": "Item Code",
  718. "店铺名称": "店铺名称",
  719. "原产地": "原产地",
  720. "产品主图": "产品主图",
  721. "上架图片1": "上架图片1",
  722. "上架图片2": "上架图片2",
  723. "上架图片3": "上架图片3",
  724. "上架图片4": "上架图片4",
  725. "上架图片5": "上架图片5",
  726. "上架图片6": "上架图片6",
  727. "上架图片7": "上架图片7",
  728. "上架图片8": "上架图片8",
  729. "产品描述": "产品描述",
  730. "产品特点1": "产品特点1",
  731. "产品特点2": "产品特点2",
  732. "产品特点3": "产品特点3",
  733. "产品特点4": "产品特点4",
  734. "产品特点5": "产品特点5",
  735. }
  736. source_map = {}
  737. matched_count = 0
  738. for key, expected in source_col_map.items():
  739. found = False
  740. for col in df.columns:
  741. if col == expected:
  742. source_map[key] = col
  743. found = True
  744. matched_count += 1
  745. break
  746. if not found:
  747. source_map[key] = None
  748. add_ai_log(f"🔗 来源列匹配: {matched_count}/{len(source_col_map)} 列匹配成功", 'info')
  749. # 填充数据
  750. def get_key(row):
  751. item = row.get("Item Code", "")
  752. name = row.get("产品英文名称", "")
  753. if pd.isna(name) or not name:
  754. name = row.get("产品名称", "")
  755. return f"{item}_{name}"
  756. keys = [get_key(row) for _, row in df.iterrows()]
  757. key_count = Counter(keys)
  758. key_index = Counter()
  759. row_num = last_row
  760. filled_count = 0
  761. add_ai_log(f"✍️ 开始填充数据,共 {total_rows} 行", 'info')
  762. for idx, (_, src_row) in enumerate(df.iterrows()):
  763. key = get_key(src_row)
  764. if key_count[key] > 1:
  765. key_index[key] += 1
  766. suffix = f".{key_index[key] - 1}"
  767. mfr_suffix = f".{key_index[key]}"
  768. else:
  769. suffix = ""
  770. mfr_suffix = ""
  771. is_parent = (idx == 0) or (key_index[key] == 1)
  772. # Parentage Level
  773. if "Parentage Level" in col_indices:
  774. if not is_parent:
  775. ws.cell(row=row_num, column=col_indices["Parentage Level"], value="Child")
  776. # Parent SKU
  777. if "Parent SKU" in col_indices:
  778. if not is_parent:
  779. first_item = df.iloc[0].get("Item Code", "")
  780. first_name = df.iloc[0].get("产品英文名称", "")
  781. if pd.isna(first_name) or not first_name:
  782. first_name = df.iloc[0].get("产品名称", "")
  783. first_sku = f"{first_item}{first_name}"
  784. ws.cell(row=row_num, column=col_indices["Parent SKU"], value=first_sku)
  785. # Variation Theme Name
  786. if "Variation Theme Name" in col_indices:
  787. if key_count[key] > 1:
  788. ws.cell(row=row_num, column=col_indices["Variation Theme Name"], value="COLOR")
  789. # 获取AI数据
  790. ai_data = ai_results.get(idx, {}) if use_ai else {}
  791. # 填充
  792. fill_count = fill_amazon_row(
  793. src_row, source_map, col_indices, valid_values,
  794. row_num, ws, suffix, mfr_suffix, ai_data
  795. )
  796. row_num += 1
  797. filled_count += 1
  798. # 每10行输出一次进度
  799. if (idx + 1) % 10 == 0:
  800. add_ai_log(f"📊 填充进度: {idx+1}/{total_rows} 行", 'info')
  801. add_ai_log(f"✅ 数据填充完成,共 {filled_count} 行", 'success')
  802. # 保存
  803. output_filename = f"output_{template_key}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
  804. output_path = os.path.join(OUTPUT_FOLDER, output_filename)
  805. wb.save(output_path)
  806. add_ai_log(f"💾 文件保存成功: {output_filename}", 'success')
  807. try:
  808. os.remove(source_path)
  809. except:
  810. pass
  811. ai_info = f",AI识别 {len(ai_results)} 行" if use_ai and ai_results else ""
  812. return jsonify({
  813. 'success': True,
  814. 'filled_count': filled_count,
  815. 'download_url': f'/download/{output_filename}',
  816. 'message': f'成功追加 {filled_count} 行数据到 {config["name"]} 模板{ai_info}',
  817. 'ai_used': use_ai,
  818. 'ai_rows': len(ai_results) if ai_results else 0,
  819. 'logs': ai_logs[-30:] # 返回最近30条日志
  820. })
  821. except Exception as e:
  822. import traceback
  823. error_detail = traceback.format_exc()
  824. add_ai_log(f"❌ 生成失败: {str(e)}", 'error')
  825. print(error_detail)
  826. return jsonify({'error': str(e), 'detail': error_detail}), 500
  827. @app.route('/download/<filename>')
  828. def download(filename):
  829. file_path = os.path.join(OUTPUT_FOLDER, filename)
  830. if not os.path.exists(file_path):
  831. return '文件不存在', 404
  832. return send_file(file_path, as_attachment=True, download_name=filename)
  833. if __name__ == '__main__':
  834. for key, config in TEMPLATE_CONFIG.items():
  835. template_path = os.path.join(TEMPLATES_FOLDER, config['file'])
  836. if not os.path.exists(template_path):
  837. print(f'⚠️ 模板文件不存在: {template_path}')
  838. app.run(host='0.0.0.0', port=5002, debug=True)