aria-code 0.52.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aria_code/__init__.py +42 -0
- aria_code/_version.py +20 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +34 -0
- aria_code/agents/base.py +318 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +303 -0
- aria_code/agents/financial/fundamental.py +191 -0
- aria_code/agents/financial/macro.py +99 -0
- aria_code/agents/financial/news.py +207 -0
- aria_code/agents/financial/risk.py +165 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +258 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +363 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +158 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +492 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +15 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +95 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/bootstrap.py +145 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +16 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +369 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +239 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
- aria_code/apps/cli/commands/broker_cmds.py +1234 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +87 -0
- aria_code/apps/cli/commands/core_cmds.py +2520 -0
- aria_code/apps/cli/commands/data_cmds.py +457 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +434 -0
- aria_code/apps/cli/commands/market_cmds.py +1381 -0
- aria_code/apps/cli/commands/market_context.py +425 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2153 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
- aria_code/apps/cli/commands/report.py +437 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +669 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
- aria_code/apps/cli/commands/workflow_cmds.py +480 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +78 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/deterministic.py +191 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +196 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
- aria_code/apps/cli/handlers/market_handlers.py +2513 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/helpers.py +302 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +297 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +276 -0
- aria_code/apps/cli/intent_signals.py +344 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +536 -0
- aria_code/apps/cli/main.py +29 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/model_catalog.py +620 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +59 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +53 -0
- aria_code/apps/cli/prompts/select.py +142 -0
- aria_code/apps/cli/prompts/system_prompts.py +552 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +521 -0
- aria_code/apps/cli/providers/chat_routing.py +128 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
- aria_code/apps/cli/providers/runtime_bridge.py +353 -0
- aria_code/apps/cli/providers/vertexai_stream.py +316 -0
- aria_code/apps/cli/runtime_consumer.py +657 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_store.py +112 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +823 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +106 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +180 -0
- aria_code/apps/cli/tools/market_tools.py +620 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +726 -0
- aria_code/apps/cli/tools/write_tools.py +845 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +153 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1597 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +6743 -0
- aria_code/aria_daemon.py +1398 -0
- aria_code/aria_feishu_bot.py +1423 -0
- aria_code/aria_mcp_server.py +7 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_relay_server.py +490 -0
- aria_code/aria_telegram_bot.py +202 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +182 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1976 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +181 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/demo_player.py +207 -0
- aria_code/doctor.py +813 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/backtest_engine.py +354 -0
- aria_code/domain/backtest_report.py +472 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/harness.py +598 -0
- aria_code/evals/runner.py +238 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +386 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +639 -0
- aria_code/memory_manager.py +246 -0
- aria_code/model_capability.py +486 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +23 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +52 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +201 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +111 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1550 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +396 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +79 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +268 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +57 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +157 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +8 -0
- aria_code/packages/aria_tools/financial/_paths.py +130 -0
- aria_code/packages/aria_tools/financial/compliance.py +31 -0
- aria_code/packages/aria_tools/financial/factors.py +33 -0
- aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
- aria_code/packages/aria_tools/financial/strategy.py +31 -0
- aria_code/packages/aria_tools/registry.py +128 -0
- aria_code/packages/quant_engine/__init__.py +34 -0
- aria_code/packages/quant_engine/_namespace.py +62 -0
- aria_code/packages/quant_engine/backtest/__init__.py +14 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +96 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +80 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/ml_model.py +377 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/logistics/__init__.py +169 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +129 -0
- aria_code/providers/llm/openai_compat.py +313 -0
- aria_code/providers/llm/registry.py +444 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +589 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/runtime/__init__.py +169 -0
- aria_code/runtime/acceptance.py +481 -0
- aria_code/runtime/agent_loop.py +1520 -0
- aria_code/runtime/approval.py +57 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +432 -0
- aria_code/runtime/crash_report.py +174 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +150 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/repo_map.py +700 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +450 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/subagent.py +547 -0
- aria_code/runtime/task_ledger.py +86 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/tool_executor.py +217 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +23 -0
- aria_code/safety/permissions.py +306 -0
- aria_code/safety/service.py +102 -0
- aria_code/setup_wizard.py +673 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +504 -0
- aria_code/tools/data_analysis_tools.py +809 -0
- aria_code/tools/enterprise_finance_tools.py +156 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +885 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3417 -0
- aria_code/tools/logistics_tools.py +135 -0
- aria_code/tools/macro_tools.py +369 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +659 -0
- aria_code/tools/spreadsheet_tools.py +388 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +508 -0
- aria_code/ui/completer.py +501 -0
- aria_code/ui/console.py +281 -0
- aria_code/ui/image_render.py +243 -0
- aria_code/ui/input_box.py +488 -0
- aria_code/ui/picker.py +201 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/finance.py +1705 -0
- aria_code/ui/render/market.py +225 -0
- aria_code/ui/render/output.py +775 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +354 -0
- aria_code/ui/robot.py +235 -0
- aria_code/ui/startup_dashboard.py +109 -0
- aria_code/utils/vscode_uri.py +37 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +271 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +193 -0
- aria_code/workspace/verify.py +113 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_context.py +233 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.52.0.dist-info/METADATA +237 -0
- aria_code-0.52.0.dist-info/RECORD +478 -0
- aria_code-0.52.0.dist-info/WHEEL +5 -0
- aria_code-0.52.0.dist-info/entry_points.txt +3 -0
- aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.52.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""State model and responsive layout policy for the startup dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import re
|
|
7
|
+
from typing import Literal, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DashboardLayout = Literal["wide", "stacked", "minimal"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def select_dashboard_layout(width: int, height: Optional[int] = None) -> DashboardLayout:
|
|
14
|
+
"""Choose a stable layout for the available terminal width."""
|
|
15
|
+
if width >= 76:
|
|
16
|
+
return "wide"
|
|
17
|
+
if height is not None and height <= 24:
|
|
18
|
+
return "minimal"
|
|
19
|
+
if width >= 64:
|
|
20
|
+
return "stacked"
|
|
21
|
+
return "minimal"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class StartupDashboardViewModel:
|
|
26
|
+
"""Display-ready startup state, independent from Rich and the CLI runtime."""
|
|
27
|
+
|
|
28
|
+
version: str
|
|
29
|
+
runtime_label: str
|
|
30
|
+
cwd: str
|
|
31
|
+
control_status: str
|
|
32
|
+
health_status: str
|
|
33
|
+
tool_count: int
|
|
34
|
+
skill_count: int
|
|
35
|
+
lang: str = "en"
|
|
36
|
+
first_run: bool = False
|
|
37
|
+
update_notice: Optional[str] = None
|
|
38
|
+
auto_healed_from: str = ""
|
|
39
|
+
current_id: str = ""
|
|
40
|
+
badge: str = ""
|
|
41
|
+
best_lite_id: str = ""
|
|
42
|
+
best_lite_installed: bool = True
|
|
43
|
+
git_branch: str = ""
|
|
44
|
+
git_dirty: bool = False
|
|
45
|
+
mcp_server_count: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def is_zh(self) -> bool:
|
|
49
|
+
return self.lang.lower().startswith("zh")
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def capabilities(self) -> str:
|
|
53
|
+
parts = []
|
|
54
|
+
if self.mcp_server_count:
|
|
55
|
+
parts.append(
|
|
56
|
+
f"MCP {self.mcp_server_count}"
|
|
57
|
+
if not self.is_zh
|
|
58
|
+
else f"MCP {self.mcp_server_count}"
|
|
59
|
+
)
|
|
60
|
+
if self.is_zh:
|
|
61
|
+
parts.extend((f"{self.tool_count} 个工具", f"{self.skill_count} 个技能"))
|
|
62
|
+
else:
|
|
63
|
+
tool_word = "tool" if self.tool_count == 1 else "tools"
|
|
64
|
+
skill_word = "skill" if self.skill_count == 1 else "skills"
|
|
65
|
+
parts.extend((f"{self.tool_count} {tool_word}", f"{self.skill_count} {skill_word}"))
|
|
66
|
+
return " · ".join(parts)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def workspace_state(self) -> str:
|
|
70
|
+
if not self.git_branch:
|
|
71
|
+
return ""
|
|
72
|
+
state = "有改动" if self.is_zh else "dirty"
|
|
73
|
+
clean = "干净" if self.is_zh else "clean"
|
|
74
|
+
return f"{self.git_branch} · {state if self.git_dirty else clean}"
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def compact_health(self) -> str:
|
|
78
|
+
plain = re.sub(r"\[/?[^]]+\]", "", self.health_status)
|
|
79
|
+
match = re.search(r"Ollama\s+(?:online|在线)\s*·\s*(\d+)", plain, re.IGNORECASE)
|
|
80
|
+
if match:
|
|
81
|
+
prefix = "本地" if self.is_zh else "Local"
|
|
82
|
+
return f"{prefix}: Ollama {match.group(1)}"
|
|
83
|
+
if "offline" in plain.lower() or "离线" in plain:
|
|
84
|
+
return "本地: Ollama 离线" if self.is_zh else "Local: Ollama offline"
|
|
85
|
+
return plain
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def getting_started_title(self) -> str:
|
|
89
|
+
return "快速开始" if self.is_zh else "Quick start"
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def runtime_title(self) -> str:
|
|
93
|
+
return "运行状态" if self.is_zh else "Runtime"
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def whats_new_title(self) -> str:
|
|
97
|
+
return "版本更新" if self.is_zh else "What's new"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def getting_started_lines(self) -> tuple[str, str]:
|
|
101
|
+
if self.is_zh:
|
|
102
|
+
return (
|
|
103
|
+
"直接描述任务即可",
|
|
104
|
+
"/plan · /help · @ 上下文",
|
|
105
|
+
)
|
|
106
|
+
return (
|
|
107
|
+
"Describe the task naturally",
|
|
108
|
+
"/plan · /help · @ context",
|
|
109
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
src/aria_code/utils/vscode_uri.py — VS Code / Cursor / Windsurf URI Protocol Generator
|
|
3
|
+
=====================================================================================
|
|
4
|
+
Generates standard deep links to open files directly in local IDEs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_ide_uri(
|
|
15
|
+
file_path: str,
|
|
16
|
+
line: int = 1,
|
|
17
|
+
column: int = 1,
|
|
18
|
+
editor: str = "vscode",
|
|
19
|
+
) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Generate deep link URI for opening a local file in VS Code, Cursor, or Windsurf.
|
|
22
|
+
|
|
23
|
+
Supported editors:
|
|
24
|
+
- "vscode" (default) -> vscode://file/{path}:{line}:{column}
|
|
25
|
+
- "cursor" -> cursor://file/{path}:{line}:{column}
|
|
26
|
+
- "windsurf" -> windsurf://file/{path}:{line}:{column}
|
|
27
|
+
- "vscodium" -> vscodium://file/{path}:{line}:{column}
|
|
28
|
+
"""
|
|
29
|
+
abs_path = os.path.abspath(os.path.expanduser(file_path))
|
|
30
|
+
scheme_map = {
|
|
31
|
+
"vscode": "vscode",
|
|
32
|
+
"cursor": "cursor",
|
|
33
|
+
"windsurf": "windsurf",
|
|
34
|
+
"vscodium": "vscodium",
|
|
35
|
+
}
|
|
36
|
+
scheme = scheme_map.get(editor.lower(), "vscode")
|
|
37
|
+
return f"{scheme}://file/{abs_path}:{line}:{column}"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Layer 2 of the video architecture: local, free AI-assisted analysis —
|
|
2
|
+
produces editing *decisions/suggestions* (transcript, scene-cut timestamps),
|
|
3
|
+
never new pixels. Both pieces run entirely on this machine:
|
|
4
|
+
|
|
5
|
+
transcribe_video() — faster-whisper (CTranslate2-backed, much lighter
|
|
6
|
+
than diffusion models: the "base" model is ~150MB, "small" ~500MB)
|
|
7
|
+
detect_scenes() — opencv (already an existing optional dependency,
|
|
8
|
+
used elsewhere for video keyframe extraction) via a histogram-diff
|
|
9
|
+
cut detector — no new heavy model, just frame comparison
|
|
10
|
+
|
|
11
|
+
Both are lazy-imported and optional, same pattern as local_image_provider.py.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import tempfile
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, List, Optional
|
|
21
|
+
|
|
22
|
+
_whisper_model_cache: Dict[str, Any] = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_ffmpeg() -> Optional[str]:
|
|
26
|
+
if shutil.which("ffmpeg") is None:
|
|
27
|
+
return "ffmpeg not found on PATH. Install it: brew install ffmpeg"
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _extract_audio(input_path: str, out_wav: str) -> Dict[str, Any]:
|
|
32
|
+
missing = _require_ffmpeg()
|
|
33
|
+
if missing:
|
|
34
|
+
return {"success": False, "error": missing}
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["ffmpeg", "-y", "-i", input_path, "-vn", "-ac", "1", "-ar", "16000", out_wav],
|
|
38
|
+
capture_output=True, text=True, timeout=120,
|
|
39
|
+
)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
return {"success": False, "error": str(exc)}
|
|
42
|
+
if result.returncode != 0:
|
|
43
|
+
return {"success": False, "error": result.stderr[-500:]}
|
|
44
|
+
return {"success": True}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_whisper_model(model_size: str):
|
|
48
|
+
if model_size in _whisper_model_cache:
|
|
49
|
+
return _whisper_model_cache[model_size]
|
|
50
|
+
try:
|
|
51
|
+
from faster_whisper import WhisperModel
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
raise RuntimeError(
|
|
54
|
+
"Transcription needs the optional 'video_analysis' extra: "
|
|
55
|
+
"pip install -e '.[video_analysis]' (adds faster-whisper). "
|
|
56
|
+
f"Missing: {exc}"
|
|
57
|
+
) from exc
|
|
58
|
+
model = WhisperModel(model_size, device="cpu", compute_type="int8")
|
|
59
|
+
_whisper_model_cache[model_size] = model
|
|
60
|
+
return model
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def transcribe_video(input_path: str, *, model_size: str = "base", language: Optional[str] = None) -> Dict[str, Any]:
|
|
64
|
+
"""Transcribe a video's speech track locally via faster-whisper.
|
|
65
|
+
|
|
66
|
+
Returns segments with timestamps (for captions/subtitles) plus the full
|
|
67
|
+
text. Blocking, CPU-bound — callers on an event loop must run this in an
|
|
68
|
+
executor, same as every other client in this codebase.
|
|
69
|
+
"""
|
|
70
|
+
if not Path(input_path).exists():
|
|
71
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
72
|
+
|
|
73
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
74
|
+
wav_path = str(Path(tmpdir) / "audio.wav")
|
|
75
|
+
extracted = _extract_audio(input_path, wav_path)
|
|
76
|
+
if not extracted["success"]:
|
|
77
|
+
return extracted
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
model = _get_whisper_model(model_size)
|
|
81
|
+
segments, info = model.transcribe(wav_path, language=language)
|
|
82
|
+
segment_list = [
|
|
83
|
+
{"start": round(s.start, 2), "end": round(s.end, 2), "text": s.text.strip()}
|
|
84
|
+
for s in segments
|
|
85
|
+
]
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
return {"success": False, "error": str(exc)}
|
|
88
|
+
|
|
89
|
+
full_text = " ".join(s["text"] for s in segment_list)
|
|
90
|
+
return {
|
|
91
|
+
"success": True,
|
|
92
|
+
"language": getattr(info, "language", language),
|
|
93
|
+
"segments": segment_list,
|
|
94
|
+
"text": full_text,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def detect_scenes(input_path: str, *, threshold: float = 30.0, sample_every_n_frames: int = 5) -> Dict[str, Any]:
|
|
99
|
+
"""Detect scene-cut timestamps via consecutive-frame histogram
|
|
100
|
+
difference — sampling every `sample_every_n_frames` rather than every
|
|
101
|
+
frame keeps this fast on longer videos at the cost of cut-timing
|
|
102
|
+
precision within that sampling window.
|
|
103
|
+
|
|
104
|
+
`threshold` is the Bhattacharyya-distance-derived diff score above
|
|
105
|
+
which two sampled frames are considered different scenes; higher =
|
|
106
|
+
less sensitive (fewer, more confident cuts).
|
|
107
|
+
"""
|
|
108
|
+
if not Path(input_path).exists():
|
|
109
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
110
|
+
try:
|
|
111
|
+
import cv2
|
|
112
|
+
except ImportError as exc:
|
|
113
|
+
return {
|
|
114
|
+
"success": False,
|
|
115
|
+
"error": (
|
|
116
|
+
"Scene detection needs the optional 'video' extra: "
|
|
117
|
+
f"pip install -e '.[video]' (adds opencv-python). Missing: {exc}"
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
cap = cv2.VideoCapture(input_path)
|
|
122
|
+
if not cap.isOpened():
|
|
123
|
+
return {"success": False, "error": f"Could not open video: {input_path}"}
|
|
124
|
+
|
|
125
|
+
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
|
126
|
+
cuts: List[float] = []
|
|
127
|
+
prev_hist = None
|
|
128
|
+
frame_idx = 0
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
while True:
|
|
132
|
+
ok, frame = cap.read()
|
|
133
|
+
if not ok:
|
|
134
|
+
break
|
|
135
|
+
if frame_idx % sample_every_n_frames == 0:
|
|
136
|
+
hist = cv2.calcHist([frame], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256])
|
|
137
|
+
cv2.normalize(hist, hist)
|
|
138
|
+
if prev_hist is not None:
|
|
139
|
+
diff = cv2.compareHist(prev_hist, hist, cv2.HISTCMP_BHATTACHARYYA) * 100
|
|
140
|
+
if diff > threshold:
|
|
141
|
+
cuts.append(round(frame_idx / fps, 2))
|
|
142
|
+
prev_hist = hist
|
|
143
|
+
frame_idx += 1
|
|
144
|
+
finally:
|
|
145
|
+
cap.release()
|
|
146
|
+
|
|
147
|
+
return {"success": True, "fps": fps, "total_frames": frame_idx, "scene_cuts_sec": cuts}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Layer 1 of the video architecture: deterministic ffmpeg-based editing —
|
|
2
|
+
trim, concat, overlay text, overlay audio, convert format/aspect, change
|
|
3
|
+
speed. No AI, no model weights, no API key — just a subprocess wrapper
|
|
4
|
+
around the `ffmpeg`/`ffprobe` CLIs, same pattern as project_tools.py's
|
|
5
|
+
`_git()` helper (timeout-bounded, capture output, never raise — return an
|
|
6
|
+
error dict instead).
|
|
7
|
+
|
|
8
|
+
Layer 2 (video_analysis.py, local Whisper transcription + opencv scene
|
|
9
|
+
detection) and Layer 3 (cloud AI generation, not built yet — needs a
|
|
10
|
+
provider/budget decision first) are separate modules.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
DEFAULT_TIMEOUT = 300.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _require_ffmpeg() -> Optional[str]:
|
|
25
|
+
if shutil.which("ffmpeg") is None:
|
|
26
|
+
return "ffmpeg not found on PATH. Install it: brew install ffmpeg"
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _run_ffmpeg(args: List[str], *, timeout: float = DEFAULT_TIMEOUT) -> Dict[str, Any]:
|
|
31
|
+
"""Run an ffmpeg command. Returns {"success": bool, "error"?: str}."""
|
|
32
|
+
missing = _require_ffmpeg()
|
|
33
|
+
if missing:
|
|
34
|
+
return {"success": False, "error": missing}
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["ffmpeg", "-y", *args],
|
|
38
|
+
capture_output=True, text=True, timeout=timeout,
|
|
39
|
+
)
|
|
40
|
+
except subprocess.TimeoutExpired:
|
|
41
|
+
return {"success": False, "error": f"ffmpeg timed out after {timeout:.0f}s"}
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
return {"success": False, "error": str(exc)}
|
|
44
|
+
if result.returncode != 0:
|
|
45
|
+
return {"success": False, "error": result.stderr[-800:]}
|
|
46
|
+
return {"success": True}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _default_output(input_path: str, suffix: str, ext: Optional[str] = None) -> Path:
|
|
50
|
+
src = Path(input_path)
|
|
51
|
+
from artifacts import create_user_artifact
|
|
52
|
+
|
|
53
|
+
artifact = create_user_artifact("video", src.stem, f"{src.stem}_{suffix}", ext or src.suffix)
|
|
54
|
+
return artifact.path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def probe_video(input_path: str) -> Dict[str, Any]:
|
|
58
|
+
"""Return duration/resolution/codec info via ffprobe."""
|
|
59
|
+
if shutil.which("ffprobe") is None:
|
|
60
|
+
return {"success": False, "error": "ffprobe not found on PATH. Install it: brew install ffmpeg"}
|
|
61
|
+
if not Path(input_path).exists():
|
|
62
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(
|
|
65
|
+
["ffprobe", "-v", "error", "-show_entries",
|
|
66
|
+
"stream=width,height,codec_type,codec_name,r_frame_rate:format=duration,size",
|
|
67
|
+
"-of", "json", input_path],
|
|
68
|
+
capture_output=True, text=True, timeout=30,
|
|
69
|
+
)
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
return {"success": False, "error": str(exc)}
|
|
72
|
+
if result.returncode != 0:
|
|
73
|
+
return {"success": False, "error": result.stderr[-500:]}
|
|
74
|
+
import json
|
|
75
|
+
return {"success": True, **json.loads(result.stdout)}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def trim_video(input_path: str, start: float, end: float, *, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
79
|
+
"""Cut [start, end] seconds out of a video (re-encodes for frame-accurate cuts)."""
|
|
80
|
+
if not Path(input_path).exists():
|
|
81
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
82
|
+
if end <= start:
|
|
83
|
+
return {"success": False, "error": "end must be greater than start"}
|
|
84
|
+
out = Path(output_path) if output_path else _default_output(input_path, "trimmed")
|
|
85
|
+
result = _run_ffmpeg([
|
|
86
|
+
"-ss", str(start), "-to", str(end), "-i", input_path,
|
|
87
|
+
"-c:v", "libx264", "-c:a", "aac", str(out),
|
|
88
|
+
])
|
|
89
|
+
if not result["success"]:
|
|
90
|
+
return result
|
|
91
|
+
return {"success": True, "path": str(out)}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def concat_videos(input_paths: List[str], *, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
95
|
+
"""Concatenate multiple videos in order. Re-encodes each to a common
|
|
96
|
+
codec first so mismatched source formats (different codec/resolution)
|
|
97
|
+
don't silently produce a broken concat — the naive ffmpeg concat demuxer
|
|
98
|
+
only works when all inputs already share a codec."""
|
|
99
|
+
for p in input_paths:
|
|
100
|
+
if not Path(p).exists():
|
|
101
|
+
return {"success": False, "error": f"File not found: {p}"}
|
|
102
|
+
if len(input_paths) < 2:
|
|
103
|
+
return {"success": False, "error": "Need at least 2 videos to concatenate"}
|
|
104
|
+
|
|
105
|
+
out = Path(output_path) if output_path else _default_output(input_paths[0], "concat")
|
|
106
|
+
filter_inputs = "".join(f"[{i}:v:0][{i}:a:0]" for i in range(len(input_paths)))
|
|
107
|
+
args = []
|
|
108
|
+
for p in input_paths:
|
|
109
|
+
args += ["-i", p]
|
|
110
|
+
args += [
|
|
111
|
+
"-filter_complex", f"{filter_inputs}concat=n={len(input_paths)}:v=1:a=1[outv][outa]",
|
|
112
|
+
"-map", "[outv]", "-map", "[outa]",
|
|
113
|
+
"-c:v", "libx264", "-c:a", "aac", str(out),
|
|
114
|
+
]
|
|
115
|
+
result = _run_ffmpeg(args)
|
|
116
|
+
if not result["success"]:
|
|
117
|
+
return result
|
|
118
|
+
return {"success": True, "path": str(out)}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def overlay_text(
|
|
122
|
+
input_path: str,
|
|
123
|
+
text: str,
|
|
124
|
+
*,
|
|
125
|
+
position: str = "bottom",
|
|
126
|
+
font_size: int = 36,
|
|
127
|
+
font_color: str = "white",
|
|
128
|
+
output_path: Optional[str] = None,
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""Burn a text overlay onto the video.
|
|
131
|
+
|
|
132
|
+
Renders the text as a transparent PNG via Pillow and composites it with
|
|
133
|
+
ffmpeg's `overlay` filter, rather than ffmpeg's own `drawtext` filter —
|
|
134
|
+
`drawtext` needs ffmpeg built with libfreetype, which isn't guaranteed
|
|
135
|
+
(confirmed absent on a stock Homebrew ffmpeg install: `No such filter:
|
|
136
|
+
'drawtext'`). Rendering the text ourselves works on any ffmpeg build.
|
|
137
|
+
"""
|
|
138
|
+
if not Path(input_path).exists():
|
|
139
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
140
|
+
|
|
141
|
+
probe = probe_video(input_path)
|
|
142
|
+
if not probe["success"]:
|
|
143
|
+
return probe
|
|
144
|
+
video_stream = next((s for s in probe.get("streams", []) if s.get("codec_type") == "video"), None)
|
|
145
|
+
if not video_stream:
|
|
146
|
+
return {"success": False, "error": "Could not determine video dimensions"}
|
|
147
|
+
width, height = int(video_stream["width"]), int(video_stream["height"])
|
|
148
|
+
|
|
149
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
150
|
+
|
|
151
|
+
overlay_img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
|
152
|
+
draw = ImageDraw.Draw(overlay_img)
|
|
153
|
+
try:
|
|
154
|
+
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size)
|
|
155
|
+
except Exception:
|
|
156
|
+
font = ImageFont.load_default()
|
|
157
|
+
|
|
158
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
159
|
+
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
160
|
+
margin = 40
|
|
161
|
+
positions = {
|
|
162
|
+
"top": ((width - text_w) // 2, margin),
|
|
163
|
+
"bottom": ((width - text_w) // 2, height - text_h - margin),
|
|
164
|
+
"center": ((width - text_w) // 2, (height - text_h) // 2),
|
|
165
|
+
}
|
|
166
|
+
x, y = positions.get(position, positions["bottom"])
|
|
167
|
+
pad = 10
|
|
168
|
+
draw.rectangle([x - pad, y - pad, x + text_w + pad, y + text_h + pad], fill=(0, 0, 0, 128))
|
|
169
|
+
draw.text((x, y), text, font=font, fill=font_color)
|
|
170
|
+
|
|
171
|
+
out = Path(output_path) if output_path else _default_output(input_path, "text")
|
|
172
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
173
|
+
overlay_path = str(Path(tmpdir) / "overlay.png")
|
|
174
|
+
overlay_img.save(overlay_path)
|
|
175
|
+
result = _run_ffmpeg([
|
|
176
|
+
"-i", input_path, "-i", overlay_path,
|
|
177
|
+
"-filter_complex", "[0:v][1:v]overlay=0:0",
|
|
178
|
+
"-c:a", "copy", str(out),
|
|
179
|
+
])
|
|
180
|
+
if not result["success"]:
|
|
181
|
+
return result
|
|
182
|
+
return {"success": True, "path": str(out)}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def overlay_audio(input_path: str, audio_path: str, *, replace: bool = False, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
186
|
+
"""Add or replace the audio track. replace=True drops the original audio
|
|
187
|
+
entirely; replace=False mixes the new track under the original."""
|
|
188
|
+
if not Path(input_path).exists():
|
|
189
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
190
|
+
if not Path(audio_path).exists():
|
|
191
|
+
return {"success": False, "error": f"File not found: {audio_path}"}
|
|
192
|
+
|
|
193
|
+
out = Path(output_path) if output_path else _default_output(input_path, "audio")
|
|
194
|
+
if replace:
|
|
195
|
+
args = ["-i", input_path, "-i", audio_path, "-map", "0:v:0", "-map", "1:a:0",
|
|
196
|
+
"-c:v", "copy", "-shortest", str(out)]
|
|
197
|
+
else:
|
|
198
|
+
args = ["-i", input_path, "-i", audio_path,
|
|
199
|
+
"-filter_complex", "[0:a][1:a]amix=inputs=2:duration=first[a]",
|
|
200
|
+
"-map", "0:v:0", "-map", "[a]", "-c:v", "copy", "-shortest", str(out)]
|
|
201
|
+
result = _run_ffmpeg(args)
|
|
202
|
+
if not result["success"]:
|
|
203
|
+
return result
|
|
204
|
+
return {"success": True, "path": str(out)}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def convert_video(
|
|
208
|
+
input_path: str,
|
|
209
|
+
*,
|
|
210
|
+
output_format: Optional[str] = None,
|
|
211
|
+
aspect: Optional[str] = None,
|
|
212
|
+
output_path: Optional[str] = None,
|
|
213
|
+
) -> Dict[str, Any]:
|
|
214
|
+
"""Convert container/codec format and/or reframe to a target aspect
|
|
215
|
+
ratio (e.g. "9:16" for vertical social, "1:1" for square) via
|
|
216
|
+
crop-to-fill — center-crops rather than letterboxing, since a cropped
|
|
217
|
+
fill is what "reframe for a platform" almost always means in practice."""
|
|
218
|
+
if not Path(input_path).exists():
|
|
219
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
220
|
+
|
|
221
|
+
ext = f".{output_format}" if output_format else None
|
|
222
|
+
out = Path(output_path) if output_path else _default_output(input_path, "converted", ext)
|
|
223
|
+
|
|
224
|
+
vf = None
|
|
225
|
+
if aspect:
|
|
226
|
+
try:
|
|
227
|
+
aw, ah = (int(x) for x in aspect.split(":"))
|
|
228
|
+
except ValueError:
|
|
229
|
+
return {"success": False, "error": "aspect must be like '9:16'"}
|
|
230
|
+
vf = f"crop='min(iw,ih*{aw}/{ah})':'min(ih,iw*{ah}/{aw})',scale=trunc(iw/2)*2:trunc(ih/2)*2"
|
|
231
|
+
|
|
232
|
+
args = ["-i", input_path]
|
|
233
|
+
if vf:
|
|
234
|
+
args += ["-vf", vf]
|
|
235
|
+
args += ["-c:v", "libx264", "-c:a", "aac", str(out)]
|
|
236
|
+
result = _run_ffmpeg(args)
|
|
237
|
+
if not result["success"]:
|
|
238
|
+
return result
|
|
239
|
+
return {"success": True, "path": str(out)}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def change_speed(input_path: str, factor: float, *, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
243
|
+
"""Speed up (factor > 1) or slow down (factor < 1) a video, audio pitch-
|
|
244
|
+
corrected. atempo only accepts 0.5-2.0 per filter pass, so factors
|
|
245
|
+
outside that range are chained across multiple atempo stages."""
|
|
246
|
+
if not Path(input_path).exists():
|
|
247
|
+
return {"success": False, "error": f"File not found: {input_path}"}
|
|
248
|
+
if factor <= 0:
|
|
249
|
+
return {"success": False, "error": "factor must be positive"}
|
|
250
|
+
|
|
251
|
+
out = Path(output_path) if output_path else _default_output(input_path, "speed")
|
|
252
|
+
atempo_stages = []
|
|
253
|
+
remaining = factor
|
|
254
|
+
while remaining > 2.0:
|
|
255
|
+
atempo_stages.append(2.0)
|
|
256
|
+
remaining /= 2.0
|
|
257
|
+
while remaining < 0.5:
|
|
258
|
+
atempo_stages.append(0.5)
|
|
259
|
+
remaining /= 0.5
|
|
260
|
+
atempo_stages.append(remaining)
|
|
261
|
+
atempo_filter = ",".join(f"atempo={s:.4f}" for s in atempo_stages)
|
|
262
|
+
|
|
263
|
+
result = _run_ffmpeg([
|
|
264
|
+
"-i", input_path,
|
|
265
|
+
"-filter_complex", f"[0:v]setpts={1/factor:.6f}*PTS[v];[0:a]{atempo_filter}[a]",
|
|
266
|
+
"-map", "[v]", "-map", "[a]",
|
|
267
|
+
"-c:v", "libx264", "-c:a", "aac", str(out),
|
|
268
|
+
])
|
|
269
|
+
if not result["success"]:
|
|
270
|
+
return result
|
|
271
|
+
return {"success": True, "path": str(out)}
|